protocol-jsonrpc 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0605256ce9da467440c5c83f3d871eef77a465a33553f9f3fbb7d5f4f4edbcd2
4
+ data.tar.gz: baa116715ac8ad29ad29f5cf188dcba524e6bcfbce4f88ecfd69288736bac692
5
+ SHA512:
6
+ metadata.gz: 8843f873055775f88cd4d5780d21ae605c3ebd688c94f700388be5edddbf3e795ceb15ba69557ddafe7a08224b7f95d52a3c1a68357fd1ea427af54a2cf64868
7
+ data.tar.gz: 7b8cd982a41aa44003629fdea9dc16d6ce447eb1cdaf3c25e26600e61dee5674490eed22f5b3183b71fcd48375270447ae4f8f29b21b8986889633840628bb3e
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-04-20
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Martin Emde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # Protocol::Jsonrpc
2
+
3
+ A Ruby library for implementing JSON-RPC 2.0 protocol clients and servers.
4
+
5
+ Design influenced by [protocol-websocket](https://github.com/socketry/protocol-websocket) by Samuel Williams ([@ioquatix](https://github.com/ioquatix)).
6
+ Thanks Samuel!
7
+
8
+ ## Installation
9
+
10
+ Add the gem to your project:
11
+
12
+ ```bash
13
+ bundle add protocol-jsonrpc
14
+ ```
15
+
16
+ If bundler is not being used to manage dependencies, install the gem by executing:
17
+
18
+ ```bash
19
+ gem install protocol-jsonrpc
20
+ ```
21
+
22
+ ## Core Concepts
23
+
24
+ Protocol::Jsonrpc has several core concepts:
25
+
26
+ - A `Protocol::Jsonrpc::Message` is the base class which represents JSON-RPC message structures
27
+ - A `Protocol::Jsonrpc::Framer` wraps an underlying stream (like a socket) for reading and writing JSON-RPC messages
28
+ - A `Protocol::Jsonrpc::Connection` wraps a framer and provides higher-level methods for communication
29
+ - Various message types including `RequestMessage`, `ResponseMessage`, `NotificationMessage`, and `ErrorMessage`
30
+
31
+ ## Basic Usage
32
+
33
+ ### Simple Example
34
+
35
+ Here's a basic example showing how to create a JSON-RPC connection over a socket pair:
36
+
37
+ ```ruby
38
+ require 'protocol/jsonrpc'
39
+ require 'protocol/jsonrpc/connection'
40
+ require 'socket'
41
+
42
+ # Create a socket pair for testing
43
+ client_socket, server_socket = UNIXSocket.pair
44
+
45
+ # Create connections for both client and server
46
+ client = Protocol::Jsonrpc::Connection.new(Protocol::Jsonrpc::Framer.new(client_socket))
47
+ server = Protocol::Jsonrpc::Connection.new(Protocol::Jsonrpc::Framer.new(server_socket))
48
+
49
+ # Client sends a request
50
+ subtract = Protocol::Jsonrpc::RequestMessage.new(method: "subtract", params: [42, 23])
51
+ client.write(subtract)
52
+
53
+ # Server reads the request
54
+ message = server.read
55
+ # => <#Protocol::Jsonrpc::RequestMessage id:"...", method: "subtract", params: [42, 23]>
56
+
57
+ # Server processes the request (calculating the result)
58
+ result = message.params.inject(:-) if message.method == "subtract"
59
+
60
+ # Server sends a response
61
+ server.write(message.reply(result))
62
+
63
+ # Client reads the response
64
+ response = client.read
65
+ response.result # => 19
66
+
67
+ # Close connections
68
+ client.close
69
+ server.close
70
+ ```
71
+
72
+ ### Realistic Server Implementation
73
+
74
+ For a more realistic server implementation:
75
+
76
+ ```ruby
77
+ require 'protocol/jsonrpc'
78
+ require 'protocol/jsonrpc/connection'
79
+ require 'socket'
80
+
81
+ server = TCPServer.new('localhost', 4567)
82
+ socket = server.accept
83
+ connection = Protocol::Jsonrpc::Connection.new(Protocol::Jsonrpc::Framer.new(socket))
84
+
85
+ # Simple dispatcher for handling different methods
86
+ handlers = {
87
+ "add" => ->(params) { params.sum },
88
+ "subtract" => ->(params) { params.reduce(:-) },
89
+ "multiply" => ->(params) { params.reduce(:*) },
90
+ "divide" => ->(params) { params.reduce(:/) }
91
+ }
92
+
93
+ # Track pending requests awaiting responses
94
+ pending_requests = {}
95
+
96
+ # Main server loop
97
+ begin
98
+ while (message = connection.read)
99
+ case message
100
+ when Protocol::Jsonrpc::RequestMessage
101
+ puts "Received request: #{message.method}"
102
+
103
+ if handlers.key?(message.method)
104
+ result = handlers[message.method].call(message.params)
105
+ connection.write(message.reply(result))
106
+ else
107
+ error = Protocol::Jsonrpc::MethodNotFoundError.new
108
+ connection.write(message.reply(error))
109
+ end
110
+
111
+ when Protocol::Jsonrpc::NotificationMessage
112
+ puts "Notification: #{message.method}"
113
+ # Handle notification (no response needed)
114
+
115
+ when Protocol::Jsonrpc::ResponseMessage
116
+ puts "Response: #{message.result}"
117
+ # Process response for an earlier request
118
+ request = pending_requests.delete(message.id)
119
+ request.call(message) if request
120
+
121
+ when Protocol::Jsonrpc::ErrorMessage
122
+ puts "Error: #{message.error.message}"
123
+ request = pending_requests.delete(message.id)
124
+ request.call(message) if request
125
+ end
126
+ end
127
+ rescue Errno::EPIPE, IOError => e
128
+ puts "Connection closed: #{e.message}"
129
+ ensure
130
+ connection.close
131
+ socket.close
132
+ server.close
133
+ end
134
+ ```
135
+
136
+ ## Message Types
137
+
138
+ ### Request Message
139
+
140
+ ```ruby
141
+ # Create a request with positional parameters
142
+ request = Protocol::Jsonrpc::RequestMessage.new(
143
+ method: "subtract",
144
+ params: [42, 23],
145
+ id: 1 # Optional, auto-generated if not provided
146
+ )
147
+
148
+ # Create a request with named parameters
149
+ request = Protocol::Jsonrpc::RequestMessage.new(
150
+ method: "subtract",
151
+ params: { minuend: 42, subtrahend: 23 },
152
+ id: 2
153
+ )
154
+ ```
155
+
156
+ ### Notification Message
157
+
158
+ Notifications are similar to requests but don't expect a response:
159
+
160
+ ```ruby
161
+ notification = Protocol::Jsonrpc::NotificationMessage.new(
162
+ method: "update",
163
+ params: [1, 2, 3, 4, 5]
164
+ )
165
+ ```
166
+
167
+ ### Response Message
168
+
169
+ Typically created by replying to a request:
170
+
171
+ ```ruby
172
+ # From a request object
173
+ response = request.reply(19)
174
+
175
+ # Or directly
176
+ response = Protocol::Jsonrpc::ResponseMessage.new(
177
+ result: 19,
178
+ id: 1
179
+ )
180
+ ```
181
+
182
+ ### Error Message
183
+
184
+ For error responses:
185
+
186
+ ```ruby
187
+ # Create from an error object
188
+ error = Protocol::Jsonrpc::InvalidParamsError.new("Invalid parameters")
189
+ error_response = request.reply(error)
190
+
191
+ # Standard error types include:
192
+ # - ParseError
193
+ # - InvalidRequestError
194
+ # - MethodNotFoundError
195
+ # - InvalidParamsError
196
+ # - InternalError
197
+ # - ServerError
198
+ ```
199
+
200
+ ## Batch Processing
201
+
202
+ JSON-RPC supports batch requests and responses:
203
+
204
+ ```ruby
205
+ batch = [
206
+ Protocol::Jsonrpc::RequestMessage.new(method: "sum", params: [1, 2, 4]),
207
+ Protocol::Jsonrpc::NotificationMessage.new(method: "notify_hello", params: [7]),
208
+ Protocol::Jsonrpc::RequestMessage.new(method: "subtract", params: [42, 23])
209
+ ]
210
+
211
+ # Send batch request
212
+ client.write(batch)
213
+
214
+ # Process batch on server
215
+ messages = server.read
216
+
217
+ batch_response = messages.filter_map do |msg|
218
+ case msg
219
+ when Protocol::Jsonrpc::RequestMessage
220
+ # Only add responses for requests, not notifications
221
+ if msg.method == "sum"
222
+ msg.reply(msg.params.sum)
223
+ elsif msg.method == "subtract"
224
+ msg.reply(msg.params.reduce(:-))
225
+ else
226
+ msg.reply(Protocol::Jsonrpc::MethodNotFoundError.new)
227
+ end
228
+ when Protocol::Jsonrpc::NotificationMessage
229
+ handle_notification(msg)
230
+ nil
231
+ end
232
+ end
233
+
234
+ # Send batch response if not empty
235
+ server.write(batch_response) unless batch_response.empty?
236
+ ```
237
+
238
+ ## Custom Framers
239
+
240
+ The supplied Framer is designed for a bidirectional socket.
241
+ You can also supply your own framer:
242
+
243
+ ```ruby
244
+ class MyFramer
245
+ def flush; end
246
+ def close; end
247
+
248
+ # Return an object that response to unpack
249
+ def read_frame
250
+ end
251
+
252
+ # Accepts a
253
+ def write_frame(frame)
254
+ end
255
+ end
256
+
257
+
258
+ client = Protocol::Jsonrpc::Connection.new(MyFramer.new)
259
+ client.read # calls read_frame, calling unpack on the returned object
260
+
261
+ message = Protocol::Jsonrpc::NotificationMessage.new(method: "hello", params: ["world"])
262
+ client.write(message) # calls write_frame with any message responding to `as_json`
263
+
264
+ ```
265
+
266
+ ## Development
267
+
268
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
269
+
270
+ ## Contributing
271
+
272
+ Bug reports and pull requests are welcome on GitHub at https://github.com/martinemde/protocol-jsonrpc.
273
+
274
+ ## License
275
+
276
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require_relative "message"
7
+
8
+ module Protocol
9
+ module Jsonrpc
10
+ class Connection
11
+ # Initialize a new Connection instance
12
+ # @param framer [Protocol::Jsonrpc::Framer] Any class implementing the Framer interface
13
+ def initialize(framer)
14
+ @framer = framer
15
+ end
16
+
17
+ def flush
18
+ @framer.flush
19
+ end
20
+
21
+ def close
22
+ @framer.close
23
+ end
24
+
25
+ # Read a message from the framer
26
+ # @yield [Protocol::Jsonrpc::Message] The read message
27
+ # @return [Protocol::Jsonrpc::Message] The read message
28
+ def read(&block)
29
+ flush
30
+ frame = read_frame
31
+ message = Message.load(frame.unpack)
32
+ yield message if block_given?
33
+ message
34
+ end
35
+
36
+ # Write a message to the framer
37
+ # @param message [Protocol::Jsonrpc::Message, Array<Protocol::Jsonrpc::Message>] The message(s) to write
38
+ # @return [Boolean] True if successful
39
+ def write(message)
40
+ frame = Frame.pack(message)
41
+ write_frame(frame)
42
+ true
43
+ end
44
+
45
+ # Low level read a frame from the framer
46
+ # @yield [Protocol::Jsonrpc::Frame] The read frame
47
+ # @return [Protocol::Jsonrpc::Frame] The read frame
48
+ def read_frame(&)
49
+ frame = @framer.read_frame
50
+ yield frame if block_given?
51
+ frame
52
+ end
53
+
54
+ # Low level write a frame to the framer
55
+ # @param frame [Protocol::Jsonrpc::Frame] The frame to write
56
+ # @return [Boolean] True if successful
57
+ def write_frame(frame)
58
+ @framer.write_frame(frame)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require "securerandom"
7
+ require "json"
8
+
9
+ module Protocol
10
+ module Jsonrpc
11
+ class Error < StandardError
12
+ PARSE_ERROR = -32_700
13
+ INVALID_REQUEST = -32_600
14
+ METHOD_NOT_FOUND = -32_601
15
+ INVALID_PARAMS = -32_602
16
+ INTERNAL_ERROR = -32_603
17
+
18
+ MESSAGES = Hash.new("Error").merge(
19
+ PARSE_ERROR => "Parse error",
20
+ INVALID_REQUEST => "Invalid Request",
21
+ METHOD_NOT_FOUND => "Method not found",
22
+ INVALID_PARAMS => "Invalid params",
23
+ INTERNAL_ERROR => "Internal error"
24
+ ).freeze
25
+
26
+ # Factory method to create the appropriate error type
27
+ # @param id [String, Integer] The request ID
28
+ # @param error [Hash] The error data from the JSON-RPC response
29
+ # @return [Error] The appropriate error instance
30
+ def self.from_message(code:, message:, data: nil, id: nil)
31
+ case code
32
+ when PARSE_ERROR
33
+ ParseError.new(message, data:, id:)
34
+ when INVALID_REQUEST
35
+ InvalidRequestError.new(message, data:, id:)
36
+ when METHOD_NOT_FOUND
37
+ MethodNotFoundError.new(message, data:, id:)
38
+ when INVALID_PARAMS
39
+ InvalidParamsError.new(message, data:, id:)
40
+ when INTERNAL_ERROR
41
+ InternalError.new(message, data:, id:)
42
+ else
43
+ new(message, data:, id:)
44
+ end
45
+ end
46
+
47
+ def self.wrap(error)
48
+ case error
49
+ in Hash
50
+ error = error.transform_keys(&:to_sym)
51
+ from_message(**error)
52
+ in Jsonrpc::Error
53
+ error
54
+ in JSON::ParserError
55
+ ParseError.new(error.message, data: error)
56
+ in StandardError
57
+ InternalError.new(error.message, data: error)
58
+ else
59
+ raise error
60
+ end
61
+ end
62
+
63
+ attr_reader :data, :code, :id
64
+
65
+ def initialize(message = nil, data: nil, id: nil)
66
+ message = nil if message&.empty?
67
+ super([MESSAGES[code], message].compact.uniq.join(": "))
68
+ @data = data
69
+ @id = id
70
+ end
71
+
72
+ def reply(id: @id)
73
+ ErrorMessage.new(id:, error: self)
74
+ end
75
+
76
+ def to_h
77
+ h = {code:, message:}
78
+ h[:data] = data if data
79
+ h
80
+ end
81
+
82
+ def [](key)
83
+ case key
84
+ when :code
85
+ code
86
+ when :message
87
+ message
88
+ when :data
89
+ data
90
+ else
91
+ raise KeyError, "Invalid key: #{key}"
92
+ end
93
+ end
94
+ end
95
+
96
+ # Error raised when a JSON-RPC parse error is received from the server
97
+ # Raised when error code is -32700
98
+ class ParseError < Error
99
+ def code
100
+ PARSE_ERROR
101
+ end
102
+ end
103
+
104
+ # Error raised when a JSON-RPC invalid request error is received from the server
105
+ # Raised when error code is -32600
106
+ class InvalidRequestError < Error
107
+ def code
108
+ INVALID_REQUEST
109
+ end
110
+ end
111
+
112
+ # Error raised when a JSON-RPC method not found error is received from the server
113
+ # Raised when error code is -32601
114
+ class MethodNotFoundError < Error
115
+ def code
116
+ METHOD_NOT_FOUND
117
+ end
118
+ end
119
+
120
+ # Error raised when a JSON-RPC invalid params error is received from the server
121
+ # Raised when error code is -32602
122
+ class InvalidParamsError < Error
123
+ def code
124
+ INVALID_PARAMS
125
+ end
126
+ end
127
+
128
+ # Error raised when a JSON-RPC internal error is received from the server
129
+ # Raised when error code is -32603
130
+ class InternalError < Error
131
+ def code
132
+ INTERNAL_ERROR
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require_relative "message"
7
+
8
+ module Protocol
9
+ module Jsonrpc
10
+ ErrorMessage = Data.define(:id, :error) do
11
+ include Message
12
+
13
+ def initialize(id:, error:)
14
+ unless id.nil? || id.is_a?(String) || id.is_a?(Numeric)
15
+ raise InvalidRequestError.new("ID must be nil, string or number", id: id)
16
+ end
17
+
18
+ error = Error.wrap(error)
19
+
20
+ super
21
+ end
22
+
23
+ def to_h = super.merge(id:, error: error.to_h)
24
+
25
+ def response? = true
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require "json"
7
+ require_relative "error"
8
+
9
+ module Protocol
10
+ module Jsonrpc
11
+ # Frame represents the raw JSON data structure of a JSON-RPC message
12
+ # before it's validated and converted into a proper Message object.
13
+ # This handles the parsing of JSON and initial structure validation.
14
+ Frame = Data.define(:json) do
15
+ # Read a frame from the stream
16
+ # @param stream [IO] The stream to read from
17
+ # @return [Frame, nil] The parsed frame or nil if the stream is empty
18
+ def self.read(stream)
19
+ json = stream.gets
20
+ return nil if json.nil?
21
+ new(json:)
22
+ end
23
+
24
+ # Unpack the json into a JSON object
25
+ # @return [Hash] The parsed JSON object
26
+ def unpack
27
+ JSON.parse(json, symbolize_names: true)
28
+ rescue JSON::ParserError => e
29
+ raise ParseError.new("Failed to parse message: #{e.message}", data: json)
30
+ end
31
+
32
+ def self.pack(message)
33
+ case message
34
+ when Array
35
+ new(json: message.map { |msg| msg.is_a?(Message) ? msg.as_json : msg }.to_json)
36
+ when Hash, Message
37
+ new(json: message.to_json)
38
+ else
39
+ raise ArgumentError, "Invalid message type: #{message.class}"
40
+ end
41
+ end
42
+
43
+ def write(stream)
44
+ stream.write("#{json}\n")
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require "protocol/jsonrpc/frame"
7
+
8
+ module Protocol
9
+ module Jsonrpc
10
+ class Framer
11
+ def initialize(stream, frame_class: Frame)
12
+ @stream = stream
13
+ @frame_class = frame_class
14
+ end
15
+
16
+ # Read a frame from the stream
17
+ # @return [Frame] The parsed frame
18
+ def read_frame(&block)
19
+ frame = @frame_class.read(@stream)
20
+ yield frame if block_given?
21
+ frame
22
+ end
23
+
24
+ # Write a frame to the stream
25
+ # @param frame [Frame] The frame to write
26
+ def write_frame(frame)
27
+ frame.write(@stream)
28
+ end
29
+
30
+ # Flush the stream
31
+ def flush
32
+ @stream.flush
33
+ end
34
+
35
+ # Close the stream
36
+ def close
37
+ @stream.close
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require "json"
7
+ require "securerandom"
8
+ require "timeout"
9
+ require_relative "error"
10
+
11
+ module Protocol
12
+ module Jsonrpc
13
+ # JsonrpcMessage provides stateless operations for creating and validating JSON-RPC messages.
14
+ # This class handles the pure functional aspects of JSON-RPC like:
15
+ # - Creating properly formatted request/notification messages
16
+ # - Validating incoming messages
17
+ # - Parsing responses and errors
18
+ module Message
19
+ JSONRPC_VERSION = "2.0"
20
+
21
+ class << self
22
+ # Validate, and return the JSON-RPC message or batch
23
+ # @param data [Hash, Array] The parsed message
24
+ # @return [Message, Array<Message>] The parsed message or batch
25
+ # @raise [ParseError] If the message cannot be parsed
26
+ # @raise [InvalidRequestError] If the message is invalid
27
+ def load(data)
28
+ case data
29
+ when Hash
30
+ from_hash(data)
31
+ when Array
32
+ from_array(data)
33
+ else
34
+ raise InvalidRequestError.new("Invalid request object", data: data.inspect)
35
+ end
36
+ end
37
+
38
+ # This is wrong. It seems like we need something more like
39
+ # an enumerator where we can run the full parse, handle, response
40
+ # cycle for each item in the array, aggregating the results and
41
+ # errors and then returning the array.
42
+ #
43
+ # The problem is that the errors should get raised and then
44
+ # handled which turns them into ErrorMessages and then the errors
45
+ # get returned to the client.
46
+ #
47
+ # Ideally this array handling would be invisible to the connection
48
+ # which would just handle one at a time with the array wrapper
49
+ # being applied by a single place that handles the batching.
50
+ def from_array(array)
51
+ raise InvalidRequestError.new("Empty batch", data: array.inspect) if array.empty?
52
+
53
+ array.map do |msg|
54
+ from_hash(msg)
55
+ rescue => e
56
+ Error.wrap(e)
57
+ end
58
+ end
59
+
60
+ # @param parsed [Hash] The parsed message
61
+ # @return [Message] The parsed message
62
+ def from_hash(parsed)
63
+ raise InvalidRequestError.new("Request is not an object", data: parsed) unless parsed.is_a?(Hash)
64
+ raise InvalidRequestError.new("Unexpected JSON-RPC version", data: parsed) unless parsed[:jsonrpc] == JSONRPC_VERSION
65
+
66
+ case parsed
67
+ in { id:, error: }
68
+ ErrorMessage.new(id:, error: Error.from_message(**error))
69
+ in { id:, result: }
70
+ ResponseMessage.new(id:, result:)
71
+ in { id:, method: }
72
+ RequestMessage.new(id:, method:, params: parsed[:params])
73
+ in { method: }
74
+ NotificationMessage.new(method:, params: parsed[:params])
75
+ else
76
+ raise ParseError.new("Unknown message: #{parsed.inspect}", data: parsed)
77
+ end
78
+ end
79
+ end
80
+
81
+ def to_h = {jsonrpc: JSONRPC_VERSION}
82
+
83
+ def to_hash = to_h
84
+
85
+ def as_json = to_h
86
+
87
+ def to_json(...) = JSON.generate(as_json, ...)
88
+
89
+ def to_s = to_json
90
+
91
+ def response? = false
92
+
93
+ def match?(message) = false
94
+
95
+ def reply(result_or_error)
96
+ if result_or_error.is_a?(StandardError)
97
+ ErrorMessage.new(id:, error: result_or_error)
98
+ else
99
+ ResponseMessage.new(id:, result: result_or_error)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require_relative "message"
7
+
8
+ module Protocol
9
+ module Jsonrpc
10
+ NotificationMessage = Data.define(:method, :params) do
11
+ include Message
12
+
13
+ def initialize(method:, params: nil)
14
+ super
15
+
16
+ unless method.is_a?(String)
17
+ raise InvalidRequestError.new("Method must be a string", data: method.inspect)
18
+ end
19
+ unless params.nil? || params.is_a?(Array) || params.is_a?(Hash)
20
+ raise InvalidRequestError.new("Params must be an array or object", data: params.inspect)
21
+ end
22
+ end
23
+
24
+ def to_h
25
+ h = super.merge(method:)
26
+ h[:params] = params if params
27
+ h
28
+ end
29
+
30
+ def id = nil
31
+
32
+ def reply = nil
33
+
34
+ def response? = false
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require "securerandom"
7
+ require_relative "message"
8
+
9
+ module Protocol
10
+ module Jsonrpc
11
+ RequestMessage = Data.define(:method, :params, :id) do
12
+ include Message
13
+
14
+ def initialize(method:, params: nil, id: SecureRandom.uuid)
15
+ unless method.is_a?(String)
16
+ raise InvalidRequestError.new("Method must be a string", data: method.inspect)
17
+ end
18
+ unless params.nil? || params.is_a?(Array) || params.is_a?(Hash)
19
+ raise InvalidRequestError.new("Params must be an array or object", data: params.inspect)
20
+ end
21
+ unless id.is_a?(String) || id.is_a?(Numeric)
22
+ raise InvalidRequestError.new("ID must be a string or number", id:)
23
+ end
24
+
25
+ super
26
+ end
27
+
28
+ def to_h
29
+ h = super.merge(id:, method:)
30
+ h[:params] = params if params
31
+ h
32
+ end
33
+
34
+ def response? = false
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require_relative "message"
7
+
8
+ module Protocol
9
+ module Jsonrpc
10
+ ResponseMessage = Data.define(:id, :result) do
11
+ include Message
12
+
13
+ def initialize(id:, result:)
14
+ unless id.nil? || id.is_a?(String) || id.is_a?(Numeric)
15
+ raise InvalidRequestError.new("ID must be nil, string or number", id:)
16
+ end
17
+
18
+ super
19
+ end
20
+
21
+ def to_h = super.merge(id:, result:)
22
+
23
+ def response? = true
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ module Protocol
7
+ module Jsonrpc
8
+ VERSION = "0.1.0"
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2025 by Martin Emde
5
+
6
+ require_relative "jsonrpc/version"
7
+ require_relative "jsonrpc/error"
8
+ require_relative "jsonrpc/message"
9
+ require_relative "jsonrpc/connection"
10
+ require_relative "jsonrpc/error_message"
11
+ require_relative "jsonrpc/notification_message"
12
+ require_relative "jsonrpc/request_message"
13
+ require_relative "jsonrpc/response_message"
14
+
15
+ module Protocol
16
+ module Jsonrpc
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ module Protocol
2
+ module Jsonrpc
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: protocol-jsonrpc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Martin Emde
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: json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.10'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.10'
26
+ description: JSON-RPC 2.0 protocol implementation
27
+ email:
28
+ - me@martinemde.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - CHANGELOG.md
34
+ - LICENSE.txt
35
+ - README.md
36
+ - lib/protocol/jsonrpc.rb
37
+ - lib/protocol/jsonrpc/connection.rb
38
+ - lib/protocol/jsonrpc/error.rb
39
+ - lib/protocol/jsonrpc/error_message.rb
40
+ - lib/protocol/jsonrpc/frame.rb
41
+ - lib/protocol/jsonrpc/framer.rb
42
+ - lib/protocol/jsonrpc/message.rb
43
+ - lib/protocol/jsonrpc/notification_message.rb
44
+ - lib/protocol/jsonrpc/request_message.rb
45
+ - lib/protocol/jsonrpc/response_message.rb
46
+ - lib/protocol/jsonrpc/version.rb
47
+ - sig/protocol/jsonrpc.rbs
48
+ homepage: https://github.com/martinemde/protocol-jsonrpc
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ allowed_push_host: https://rubygems.org
53
+ homepage_uri: https://github.com/martinemde/protocol-jsonrpc
54
+ source_code_uri: https://github.com/martinemde/protocol-jsonrpc
55
+ changelog_uri: https://github.com/martinemde/protocol-jsonrpc/blob/main/CHANGELOG.md
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 3.1.0
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.6.7
71
+ specification_version: 4
72
+ summary: JSON-RPC 2.0 protocol implementation
73
+ test_files: []