Module: Protocol::Jsonrpc

Defined in:
lib/protocol/jsonrpc.rb,
lib/protocol/jsonrpc/batch.rb,
lib/protocol/jsonrpc/error.rb,
lib/protocol/jsonrpc/frame.rb,
lib/protocol/jsonrpc/framer.rb,
lib/protocol/jsonrpc/message.rb,
lib/protocol/jsonrpc/request.rb,
lib/protocol/jsonrpc/version.rb,
lib/protocol/jsonrpc/response.rb,
lib/protocol/jsonrpc/connection.rb,
lib/protocol/jsonrpc/notification.rb,
lib/protocol/jsonrpc/error_response.rb,
lib/protocol/jsonrpc/invalid_message.rb

Defined Under Namespace

Modules: Message Classes: Connection, Error, Framer, InternalError, InvalidParamsError, InvalidRequestError, MethodNotFoundError, ParseError

Constant Summary collapse

JSONRPC_VERSION =
"2.0"
Batch =
Data.define(:messages) do
  def self.load(data)
    return InvalidMessage.new(data: data.inspect) if data.empty?

    messages = data.map { |message| Message.load(message) }
    new(messages)
  end

  def to_a = messages
  alias_method :to_ary, :to_a

  def as_json = to_a.map(&:as_json)

  def to_json(...) = JSON.generate(to_a.map(&:as_json), ...)
  alias_method :to_s, :to_json

  def reply(&block)
    to_a.filter_map do |message|
      message.reply(&block)
    end
  end

  private

  def method_missing(method, *args, **kwargs, &block)
    if messages.respond_to?(method)
      messages.send(method, *args, **kwargs, &block)
    else
      super
    end
  end

  def respond_to_missing?(method, include_private = false)
    messages.respond_to?(method, include_private) || super
  end
end
Frame =

Frame represents the raw JSON data structure of a JSON-RPC message before it’s validated and converted into a proper Message object. Handles translation between JSON strings and Ruby Hashes and reading and writing to a stream.

Data.define(:raw_json) do
  class << self
    # Read a frame from the stream
    # @param stream [IO] An objects that responds to `gets` and returns a String
    # @return [Frame, nil] The parsed frame or nil if the stream is empty
    def read(stream)
      raw_json = stream.gets
      return nil if raw_json.nil?
      new(raw_json: raw_json.strip)
    end

    # Pack a message into a frame
    # @param message [Message, Array<Message>] The message to pack
    # @return [Frame] an instance that can be written to a stream
    # @raise [ArgumentError] if the message is not a Message or Array of Messages
    def pack(message)
      if message.is_a?(Array)
        new(raw_json: message.map { |msg| as_json(msg) }.to_json)
      else
        new(raw_json: as_json(message).to_json)
      end
    end

    private def as_json(message)
      return message if message.is_a?(Hash)
      return message.as_json if message.respond_to?(:as_json)
      raise ArgumentError, "Invalid message type: #{message.class}. Must be a Hash or respond to :as_json."
    end
  end

  # Unpack the raw_json into a Hash representing the JSON object
  # Symbolizes the keys of the Hash.
  # @return [Hash] The parsed JSON object
  # @raise [ParseError] if the JSON is invalid
  def unpack
    JSON.parse(raw_json, symbolize_names: true)
  end

  def to_json(...) = raw_json

  def to_s = raw_json

  # Write the frame to a stream
  # @param stream [IO] The stream to write to
  # @return [void]
  def write(stream)
    stream.write("#{raw_json}\n")
  end
end
Request =
Data.define(:method, :params, :id, :jsonrpc) do
  include Message

  def initialize(method:, params: nil, id: SecureRandom.uuid, jsonrpc: JSONRPC_VERSION)
    unless method.is_a?(String)
      raise InvalidRequestError.new("Method must be a string", data: method.inspect)
    end
    unless params.nil? || params.is_a?(Array) || params.is_a?(Hash)
      raise InvalidRequestError.new("Params must be an array or object", data: params.inspect)
    end
    unless id.is_a?(String) || id.is_a?(Numeric)
      raise InvalidRequestError.new("ID must be a string or number", id:)
    end

    super
  end

  def to_h
    h = super
    h.delete(:params) if params.nil?
    h
  end

  def request? = true

  def reply(*args, &)
    if args.empty? && block_given?
      begin
        result_or_error = yield self
      rescue => error
        return ErrorResponse.new(id:, error:)
      end
    elsif args.length == 1
      result_or_error = args.first
    else
      raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 0 or 1)"
    end

    if result_or_error.is_a?(StandardError)
      ErrorResponse.new(id:, error: result_or_error)
    else
      Response.new(id:, result: result_or_error)
    end
  end
end
VERSION =
"0.2.1"
Response =
Data.define(:id, :result, :jsonrpc) do
  include Message

  def initialize(id:, result:, jsonrpc: JSONRPC_VERSION)
    unless id.nil? || id.is_a?(String) || id.is_a?(Numeric)
      raise InvalidRequestError.new("ID must be nil, string, or number", id:)
    end

    super
  end

  def response? = true
end
Notification =
Data.define(:method, :params, :jsonrpc) do
  include Message

  def initialize(method:, params: nil, jsonrpc: JSONRPC_VERSION)
    super

    unless method.is_a?(String)
      raise InvalidRequestError.new("Method must be a string", data: method.inspect)
    end
    unless params.nil? || params.is_a?(Array) || params.is_a?(Hash)
      raise InvalidRequestError.new("Params must be an array or object", data: params.inspect)
    end
  end

  def to_h
    h = super
    h.delete(:params) if params.nil?
    h
  end

  # Compatibility with the Message interface, Notifications have no ID
  def id = nil

  # Compatibility with the Request
  # Yields the notification for processing but ignores the result
  def reply(*, &)
    yield self if block_given?
    nil
  rescue
    nil # JSON-RPC 2.0 spec says notifications should never return, even on error.
  end

  def notification? = true
end
ErrorResponse =
Data.define(:id, :error, :jsonrpc) do
  include Message

  def initialize(id:, error:, jsonrpc: JSONRPC_VERSION)
    unless id.nil? || id.is_a?(String) || id.is_a?(Numeric)
      raise InvalidRequestError.new("ID must be nil, string or number", id: id)
    end

    error = Error.wrap(error)

    super
  end

  def to_h = super.merge(error: error.to_h)

  def error? = true

  def response? = true
end
InvalidMessage =

When the message received is not valid JSON or not a valid JSON-RPC message, this class is returned in place of a normal Message. The error that would have been raised is returned as the error. This simplifies batch processing because invalid messages in the batch can be processed as part of the batch rather than raising and interrupting the batch processing.

Data.define(:error, :id) do
  include Message

  def initialize(error: nil, data: nil, id: nil)
    error = Error.wrap(error, data:, id:)
    super(error:, id: error.id)
  end

  def invalid? = true

  def as_json = raise "InvalidMessage cannot be serialized"

  def reply(...) = ErrorResponse.new(id:, error:)

  def to_json(...) = raise "InvalidMessage cannot be serialized"

  def to_s = raise "InvalidMessage cannot be serialized"
end