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.
- checksums.yaml +7 -0
- data/.claude/settings.local.json +9 -0
- data/.editorconfig +11 -0
- data/.overcommit.yml +31 -0
- data/.rspec +3 -0
- data/.rubocop.yml +74 -0
- data/.tool-versions +1 -0
- data/.yardstick.yml +22 -0
- data/CHANGELOG.md +37 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Guardfile +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +248 -0
- data/Rakefile +41 -0
- data/Steepfile +7 -0
- data/docs/JSON-RPC-2.0-Specification.md +278 -0
- data/examples/procedures.rb +55 -0
- data/examples/rack/Gemfile +8 -0
- data/examples/rack/Gemfile.lock +68 -0
- data/examples/rack/README.md +7 -0
- data/examples/rack/app.rb +48 -0
- data/examples/rack/config.ru +19 -0
- data/examples/rack-echo/Gemfile +8 -0
- data/examples/rack-echo/Gemfile.lock +68 -0
- data/examples/rack-echo/README.md +7 -0
- data/examples/rack-echo/app.rb +43 -0
- data/examples/rack-echo/config.ru +18 -0
- data/lib/jsonrpc/batch_request.rb +102 -0
- data/lib/jsonrpc/batch_response.rb +85 -0
- data/lib/jsonrpc/configuration.rb +85 -0
- data/lib/jsonrpc/error.rb +96 -0
- data/lib/jsonrpc/errors/internal_error.rb +27 -0
- data/lib/jsonrpc/errors/invalid_params_error.rb +27 -0
- data/lib/jsonrpc/errors/invalid_request_error.rb +31 -0
- data/lib/jsonrpc/errors/method_not_found_error.rb +31 -0
- data/lib/jsonrpc/errors/parse_error.rb +29 -0
- data/lib/jsonrpc/helpers.rb +83 -0
- data/lib/jsonrpc/middleware.rb +190 -0
- data/lib/jsonrpc/notification.rb +94 -0
- data/lib/jsonrpc/parser.rb +176 -0
- data/lib/jsonrpc/request.rb +112 -0
- data/lib/jsonrpc/response.rb +127 -0
- data/lib/jsonrpc/validator.rb +140 -0
- data/lib/jsonrpc/version.rb +5 -0
- data/lib/jsonrpc.rb +25 -0
- data/sig/jsonrpc/middleware.rbs +6 -0
- data/sig/jsonrpc/parser.rbs +7 -0
- data/sig/jsonrpc.rbs +164 -0
- 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
|
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
|
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: []
|