jsonrpc-middleware 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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/settings.local.json +9 -0
  3. data/.editorconfig +11 -0
  4. data/.overcommit.yml +31 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +74 -0
  7. data/.tool-versions +1 -0
  8. data/.yardstick.yml +22 -0
  9. data/CHANGELOG.md +37 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/Guardfile +22 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +248 -0
  14. data/Rakefile +41 -0
  15. data/Steepfile +7 -0
  16. data/docs/JSON-RPC-2.0-Specification.md +278 -0
  17. data/examples/procedures.rb +55 -0
  18. data/examples/rack/Gemfile +8 -0
  19. data/examples/rack/Gemfile.lock +68 -0
  20. data/examples/rack/README.md +7 -0
  21. data/examples/rack/app.rb +48 -0
  22. data/examples/rack/config.ru +19 -0
  23. data/examples/rack-echo/Gemfile +8 -0
  24. data/examples/rack-echo/Gemfile.lock +68 -0
  25. data/examples/rack-echo/README.md +7 -0
  26. data/examples/rack-echo/app.rb +43 -0
  27. data/examples/rack-echo/config.ru +18 -0
  28. data/lib/jsonrpc/batch_request.rb +102 -0
  29. data/lib/jsonrpc/batch_response.rb +85 -0
  30. data/lib/jsonrpc/configuration.rb +85 -0
  31. data/lib/jsonrpc/error.rb +96 -0
  32. data/lib/jsonrpc/errors/internal_error.rb +27 -0
  33. data/lib/jsonrpc/errors/invalid_params_error.rb +27 -0
  34. data/lib/jsonrpc/errors/invalid_request_error.rb +31 -0
  35. data/lib/jsonrpc/errors/method_not_found_error.rb +31 -0
  36. data/lib/jsonrpc/errors/parse_error.rb +29 -0
  37. data/lib/jsonrpc/helpers.rb +83 -0
  38. data/lib/jsonrpc/middleware.rb +190 -0
  39. data/lib/jsonrpc/notification.rb +94 -0
  40. data/lib/jsonrpc/parser.rb +176 -0
  41. data/lib/jsonrpc/request.rb +112 -0
  42. data/lib/jsonrpc/response.rb +127 -0
  43. data/lib/jsonrpc/validator.rb +140 -0
  44. data/lib/jsonrpc/version.rb +5 -0
  45. data/lib/jsonrpc.rb +25 -0
  46. data/sig/jsonrpc/middleware.rbs +6 -0
  47. data/sig/jsonrpc/parser.rbs +7 -0
  48. data/sig/jsonrpc.rbs +164 -0
  49. metadata +120 -0
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONRPC
4
+ # Validates JSON-RPC 2.0 requests and notifications.
5
+ class Validator
6
+ # Validates a single request, notification or a batch of requests and/or notifications.
7
+ #
8
+ # @param [JSONRPC::BatchRequest, JSONRPC::Request, JSONRPC::Notification] batch_or_request
9
+ #
10
+ # @example Validate a single request
11
+ # validator = JSONRPC::Validator.new
12
+ # error = validator.validate(request)
13
+ #
14
+ # @example Validate a batch of requests
15
+ # validator = JSONRPC::Validator.new
16
+ # errors = validator.validate(batch)
17
+ #
18
+ # @return [JSONRPC::Error, Array<JSONRPC::Error>, nil]
19
+ #
20
+ def validate(batch_or_request)
21
+ case batch_or_request
22
+ when BatchRequest
23
+ validate_batch_params(batch_or_request)
24
+ when Request, Notification
25
+ validate_request_params(batch_or_request)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def validate_batch_params(batch)
32
+ errors = batch.map { |req| validate_request_params(req) }
33
+
34
+ # Return the array of errors (with nil for successful validations)
35
+ # If all validations passed, return nil
36
+ errors.any? ? errors : nil
37
+ end
38
+
39
+ # @param [Request, Notification] request_or_notification
40
+ def validate_request_params(request_or_notification)
41
+ config = JSONRPC.configuration
42
+
43
+ unless config.procedure?(request_or_notification.method)
44
+ return MethodNotFoundError.new(
45
+ request_id: extract_request_id(request_or_notification),
46
+ data: {
47
+ method: request_or_notification.method
48
+ }
49
+ )
50
+ end
51
+
52
+ procedure = config.get_procedure(request_or_notification.method)
53
+
54
+ # Determine params to validate based on procedure configuration
55
+ params_to_validate = prepare_params_for_validation(request_or_notification, procedure)
56
+
57
+ # If params preparation failed, return error
58
+ return params_to_validate if params_to_validate.is_a?(InvalidParamsError)
59
+
60
+ # Validate the parameters
61
+ validation_result = procedure.contract.call(params_to_validate)
62
+
63
+ unless validation_result.success?
64
+ return InvalidParamsError.new(
65
+ request_id: extract_request_id(request_or_notification),
66
+ data: {
67
+ method: request_or_notification.method,
68
+ params: validation_result.errors.to_h
69
+ }
70
+ )
71
+ end
72
+
73
+ nil
74
+ rescue StandardError => e
75
+ if ENV['DEBUG_RACK'] == 'true'
76
+ puts "Validation error: #{e.message}"
77
+ puts e.backtrace.join("\n")
78
+ end
79
+
80
+ InternalError.new(request_id: extract_request_id(request_or_notification))
81
+ end
82
+
83
+ def prepare_params_for_validation(request, procedure)
84
+ if procedure.allow_positional_arguments
85
+ handle_positional_arguments(request, procedure)
86
+ else
87
+ handle_named_arguments(request)
88
+ end
89
+ end
90
+
91
+ def handle_positional_arguments(request_or_notification, procedure)
92
+ case request_or_notification.params
93
+ when Array
94
+ # Convert positional to named parameters if procedure has a parameter name
95
+ if procedure.parameter_name
96
+ { procedure.parameter_name => request_or_notification.params }
97
+ else
98
+ {}
99
+ end
100
+ when Hash
101
+ # Named parameters are also allowed when positional arguments are enabled
102
+ request_or_notification.params
103
+ when nil
104
+ # Missing params - let the contract validation handle it
105
+ {}
106
+ else
107
+ # Invalid params type (not Array, Hash, or nil)
108
+ InvalidParamsError.new(
109
+ request_id: extract_request_id(request_or_notification),
110
+ data: { method: request_or_notification.method }
111
+ )
112
+ end
113
+ end
114
+
115
+ def handle_named_arguments(request_or_notification)
116
+ case request_or_notification.params
117
+ when Hash
118
+ request_or_notification.params
119
+ when nil
120
+ # Missing params - let the contract validation handle it
121
+ {}
122
+ else
123
+ # Invalid params type and positional arguments aren't allowed for this procedure
124
+ InvalidParamsError.new(
125
+ request_id: extract_request_id(request_or_notification),
126
+ data: { method: request_or_notification.method }
127
+ )
128
+ end
129
+ end
130
+
131
+ def extract_request_id(request_or_notification)
132
+ case request_or_notification
133
+ when Request
134
+ request_or_notification.id
135
+ when Notification
136
+ nil
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONRPC
4
+ VERSION = '0.1.0'
5
+ end
data/lib/jsonrpc.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ require 'dry-validation'
5
+
6
+ Dry::Validation.load_extensions(:predicates_as_macros)
7
+
8
+ # Encapsulates all the gem's logic
9
+ module JSONRPC
10
+ def self.configure(&)
11
+ Configuration.instance.instance_eval(&)
12
+ end
13
+
14
+ def self.configuration
15
+ Configuration.instance
16
+ end
17
+ end
18
+
19
+ loader = Zeitwerk::Loader.for_gem
20
+ loader.log! if ENV['DEBUG_ZEITWERK'] == 'true'
21
+ loader.enable_reloading
22
+ loader.collapse("#{__dir__}/jsonrpc/errors")
23
+ loader.inflector.inflect('jsonrpc' => 'JSONRPC')
24
+ loader.setup
25
+ loader.eager_load
@@ -0,0 +1,6 @@
1
+ module JSONRPC
2
+ module Middleware
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module JSONRPC
2
+ class Parser
3
+ private
4
+
5
+ def parse_batch: (Array[Request | Notification] data) -> BatchRequest
6
+ end
7
+ end
data/sig/jsonrpc.rbs ADDED
@@ -0,0 +1,164 @@
1
+ module JSONRPC
2
+ VERSION: String
3
+
4
+ # Method definitions that should be available
5
+ interface _ToJson
6
+ def to_json: (*untyped) -> String
7
+ end
8
+
9
+ interface _HashLike
10
+ def []: (Symbol) -> untyped
11
+ def []=: (Symbol, untyped) -> untyped
12
+ end
13
+
14
+ # Representing JSON-compatible types
15
+ type json_scalar = String | Integer | Float | bool | nil
16
+ type json_object = Hash[String, json_value]
17
+ type json_array = Array[json_value]
18
+ type json_value = json_scalar | json_object | json_array
19
+
20
+ # Common types used across the library
21
+ type params_type = Hash[untyped, untyped] | Array[untyped] | nil
22
+ type id_type = String | Integer | nil
23
+ type data_type = Hash[untyped, untyped] | Array[untyped] | String | Numeric | bool | nil
24
+
25
+ # Hash with Symbol keys
26
+ type symbol_hash = Hash[Symbol, untyped] & _ToJson
27
+
28
+ class Error < StandardError
29
+ attr_reader code: Integer
30
+ attr_reader message: String
31
+ attr_reader data: data_type
32
+ attr_accessor request_id: Integer | String | nil
33
+
34
+ def initialize: (code: Integer, message: String, ?data: data_type, ?request_id: id_type) -> void
35
+ def to_h: -> symbol_hash
36
+ def to_json: (*untyped) -> String
37
+
38
+ private
39
+
40
+ def validate_code: (Integer) -> void
41
+ def validate_message: (String) -> void
42
+ end
43
+
44
+ class ParseError < Error
45
+ def initialize: (?data: data_type, ?request_id: id_type) -> void
46
+ end
47
+
48
+ class InvalidRequestError < Error
49
+ def initialize: (?data: data_type, ?request_id: id_type) -> void
50
+ end
51
+
52
+ class MethodNotFoundError < Error
53
+ def initialize: (?data: data_type, ?request_id: id_type) -> void
54
+ end
55
+
56
+ class InvalidParamsError < Error
57
+ def initialize: (?data: data_type, ?request_id: id_type) -> void
58
+ end
59
+
60
+ class InternalError < Error
61
+ def initialize: (?data: data_type, ?request_id: id_type) -> void
62
+ end
63
+
64
+ class Request
65
+ attr_reader jsonrpc: String
66
+ attr_reader method: String
67
+ attr_reader params: params_type
68
+ attr_reader id: id_type
69
+
70
+ def initialize: (method: String, ?params: params_type, id: id_type) -> void
71
+ def to_h: -> symbol_hash
72
+ def to_json: (*untyped) -> String
73
+
74
+ private
75
+
76
+ def validate_method: (String) -> void
77
+ def validate_params: (params_type) -> void
78
+ def validate_id: (id_type) -> void
79
+ end
80
+
81
+ class Response
82
+ attr_reader jsonrpc: String
83
+ attr_reader result: untyped
84
+ attr_reader error: Error?
85
+ attr_reader id: id_type
86
+
87
+ def initialize: (?result: untyped, ?error: Error?, id: id_type) -> void
88
+ def success?: -> bool
89
+ def error?: -> bool
90
+ def to_h: -> symbol_hash
91
+ def to_json: (*untyped) -> String
92
+
93
+ private
94
+
95
+ def validate_result_and_error: (untyped, Error?) -> void
96
+ def validate_id: (id_type) -> void
97
+ end
98
+
99
+ class Notification
100
+ attr_reader jsonrpc: String
101
+ attr_reader method: String
102
+ attr_reader params: params_type
103
+
104
+ def initialize: (method: String, ?params: params_type) -> void
105
+ def to_h: -> symbol_hash
106
+ def to_json: (*untyped) -> String
107
+
108
+ private
109
+ def validate_method: (String) -> void
110
+ def validate_params: (params_type) -> void
111
+ end
112
+
113
+ class BatchRequest
114
+ include Enumerable[Request | Notification]
115
+
116
+ attr_reader requests: Array[Request | Notification]
117
+
118
+ def initialize: (Array[Request | Notification]) -> void
119
+ def to_h: -> Array[symbol_hash]
120
+ def to_json: (*untyped) -> String
121
+ def each: () { (Request | Notification) -> void } -> self
122
+ | () -> Enumerator[Request | Notification, self]
123
+
124
+ private
125
+
126
+ def validate_requests: (Array[untyped]) -> void
127
+ end
128
+
129
+ class BatchResponse
130
+ include Enumerable[Response]
131
+
132
+ attr_reader responses: Array[Response]
133
+
134
+ def initialize: (Array[Response]) -> void
135
+ def to_h: -> Array[symbol_hash]
136
+ def to_json: (*untyped) -> String
137
+ def each: () { (Response) -> void } -> self
138
+ | () -> Enumerator[Response, self]
139
+
140
+ private
141
+
142
+ def validate_responses: (Array[untyped]) -> void
143
+ end
144
+
145
+ module Errors
146
+ class Error = JSONRPC::Error
147
+ end
148
+ end
149
+
150
+ # External library signatures
151
+ module Zeitwerk
152
+ class Loader
153
+ def self.for_gem: () -> Loader
154
+ def enable_reloading: () -> void
155
+ def collapse: (String) -> void
156
+ def setup: () -> void
157
+ def eager_load: () -> void
158
+ end
159
+ end
160
+
161
+ module JSON
162
+ def self.parse: (String, ?untyped) -> untyped
163
+ def self.generate: (untyped, ?untyped) -> String
164
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jsonrpc-middleware
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wilson Silva
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-validation
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.11'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.11'
26
+ - !ruby/object:Gem::Dependency
27
+ name: zeitwerk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.7'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.7'
40
+ description: Implements the JSON-RPC 2.0 protocol, enabling standardized remote procedure
41
+ calls encoded in JSON.
42
+ email:
43
+ - wilson.dsigns@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".claude/settings.local.json"
49
+ - ".editorconfig"
50
+ - ".overcommit.yml"
51
+ - ".rspec"
52
+ - ".rubocop.yml"
53
+ - ".tool-versions"
54
+ - ".yardstick.yml"
55
+ - CHANGELOG.md
56
+ - CODE_OF_CONDUCT.md
57
+ - Guardfile
58
+ - LICENSE.txt
59
+ - README.md
60
+ - Rakefile
61
+ - Steepfile
62
+ - docs/JSON-RPC-2.0-Specification.md
63
+ - examples/procedures.rb
64
+ - examples/rack-echo/Gemfile
65
+ - examples/rack-echo/Gemfile.lock
66
+ - examples/rack-echo/README.md
67
+ - examples/rack-echo/app.rb
68
+ - examples/rack-echo/config.ru
69
+ - examples/rack/Gemfile
70
+ - examples/rack/Gemfile.lock
71
+ - examples/rack/README.md
72
+ - examples/rack/app.rb
73
+ - examples/rack/config.ru
74
+ - lib/jsonrpc.rb
75
+ - lib/jsonrpc/batch_request.rb
76
+ - lib/jsonrpc/batch_response.rb
77
+ - lib/jsonrpc/configuration.rb
78
+ - lib/jsonrpc/error.rb
79
+ - lib/jsonrpc/errors/internal_error.rb
80
+ - lib/jsonrpc/errors/invalid_params_error.rb
81
+ - lib/jsonrpc/errors/invalid_request_error.rb
82
+ - lib/jsonrpc/errors/method_not_found_error.rb
83
+ - lib/jsonrpc/errors/parse_error.rb
84
+ - lib/jsonrpc/helpers.rb
85
+ - lib/jsonrpc/middleware.rb
86
+ - lib/jsonrpc/notification.rb
87
+ - lib/jsonrpc/parser.rb
88
+ - lib/jsonrpc/request.rb
89
+ - lib/jsonrpc/response.rb
90
+ - lib/jsonrpc/validator.rb
91
+ - lib/jsonrpc/version.rb
92
+ - sig/jsonrpc.rbs
93
+ - sig/jsonrpc/middleware.rbs
94
+ - sig/jsonrpc/parser.rbs
95
+ homepage: https://github.com/wilsonsilva/jsonrpc-middleware
96
+ licenses:
97
+ - MIT
98
+ metadata:
99
+ homepage_uri: https://github.com/wilsonsilva/jsonrpc-middleware
100
+ source_code_uri: https://github.com/wilsonsilva/jsonrpc-middleware
101
+ changelog_uri: https://github.com/wilsonsilva/jsonrpc-middleware/blob/main/CHANGELOG.md
102
+ rubygems_mfa_required: 'true'
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 3.4.0
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubygems_version: 3.6.9
118
+ specification_version: 4
119
+ summary: Implementation of the JSON-RPC protocol.
120
+ test_files: []