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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be698b3797653f7d1139c034b97f4be0895cc181f840c4fe3d9edfb9624fb0a2
4
- data.tar.gz: 3276fd9d13964cf6fe92f9a64ffe5bfac21bef767b3fde94328bc744ca2c8731
3
+ metadata.gz: a45b1d353d7fd1547e7e984edba9c9ed1e02669914c00294d08f259cb5bb76e6
4
+ data.tar.gz: 25d2b7802a03378c90e181d5e5b580489299d41a3070817d762b43063a9d68b3
5
5
  SHA512:
6
- metadata.gz: e4da3b7c0bd1fedc61baa9b8c4e9dc7fbd90c60c3e8509a676bd72b35d224a5928280bb56c5c4a7c92a40e5958544b81544e8461a551be818b8dd41b433416c0
7
- data.tar.gz: cd7f389f4bf622cd9191c19dbba17890fa260259bd590d8d4bf9629dfc49f7573a05b772f25050c283fedc3b7f1d3899828ae1009a3b5deb52bc9911264c94ab
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
+
@@ -3,7 +3,8 @@
3
3
  "allow": [
4
4
  "Bash(bundle exec rspec:*)",
5
5
  "Bash(bundle exec rubocop:*)",
6
- "Bash(bundle exec rake:*)"
6
+ "Bash(bundle exec rake:*)",
7
+ "Bash(mkdir:*)"
7
8
  ]
8
9
  },
9
10
  "enableAllProjectMcpServers": false
data/.rubocop.yml CHANGED
@@ -66,6 +66,9 @@ RSpec/ExampleLength:
66
66
  RSpec/NestedGroups:
67
67
  Enabled: false
68
68
 
69
+ RSpec/MultipleMemoizedHelpers:
70
+ Enabled: false
71
+
69
72
  RSpec/MultipleExpectations:
70
73
  Enabled: false
71
74
 
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
- ## [Unreleased]
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
- [Unreleased]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.2.0...HEAD
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
- # JSONRPC::Middleware
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
  [![Gem Version](https://badge.fury.io/rb/jsonrpc-middleware.svg)](https://badge.fury.io/rb/jsonrpc-middleware)
4
14
  ![Build](https://github.com/wilsonsilva/jsonrpc-middleware/actions/workflows/main.yml/badge.svg)
5
15
  [![Maintainability](https://qlty.sh/badges/a275de81-94e3-45af-9469-523aa5345871/maintainability.svg)](https://qlty.sh/gh/wilsonsilva/projects/jsonrpc-middleware)
6
16
  [![Code Coverage](https://qlty.sh/badges/a275de81-94e3-45af-9469-523aa5345871/test_coverage.svg)](https://qlty.sh/gh/wilsonsilva/projects/jsonrpc-middleware)
7
17
 
8
- A Ruby implementation of the JSON-RPC protocol, enabling standardized communication between systems via remote procedure
9
- calls encoded in JSON.
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
- result = handle_single(jsonrpc_request)
82
- jsonrpc_response(result)
110
+ sum = add(jsonrpc_request.params)
111
+ jsonrpc_response(sum)
83
112
  elsif jsonrpc_notification?
84
- handle_single(jsonrpc_notification)
113
+ add(jsonrpc_notification.params)
85
114
  jsonrpc_notification_response
86
115
  else
87
- responses = handle_batch(jsonrpc_batch)
88
- jsonrpc_batch_response(responses)
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 handle_single(request_or_notification)
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 handle_batch(batch)
102
- batch.flat_map do |request_or_notification|
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 do |task|
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)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.2.0)
4
+ jsonrpc-middleware (0.3.0)
5
5
  dry-validation (~> 1.11)
6
6
  zeitwerk (~> 2.7)
7
7
 
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.flat_map do |request_or_notification|
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
@@ -27,10 +27,7 @@ class App
27
27
  end
28
28
 
29
29
  def handle_batch(batch)
30
- batch.flat_map do |request_or_notification|
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.flat_map do |request_or_notification|
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.flat_map do |request_or_notification|
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.flat_map do |request_or_notification|
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.flat_map do |request_or_notification|
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.flat_map do |request_or_notification|
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
@@ -11,5 +11,5 @@ module JSONRPC
11
11
  #
12
12
  # @return [String] The current version number
13
13
  #
14
- VERSION = '0.3.0'
14
+ VERSION = '0.4.0'
15
15
  end
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.3.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: Implements the JSON-RPC 2.0 protocol, enabling standardized remote procedure
41
- calls encoded in JSON.
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: Implementation of the JSON-RPC protocol.
171
+ summary: Rack middleware implementing the JSON-RPC 2.0 protocol.
171
172
  test_files: []