Skip to content

Instantly share code, notes, and snippets.

@ginjo
Last active June 11, 2023 00:02
Show Gist options
  • Save ginjo/5b128c655e79e1b4e0e10c27a5098177 to your computer and use it in GitHub Desktop.
Save ginjo/5b128c655e79e1b4e0e10c27a5098177 to your computer and use it in GitHub Desktop.
A Ruby class to create and manage JSON-RPC data structures
# frozen_string_literal: true
# This gemspec currently describes a gem hosted on Github as a Gist only.
# See https://bundler.io/guides/git.html
require_relative "json_rpc_object_version.rb"
Gem::Specification.new do |spec|
spec.name = "json_rpc_object"
spec.version = JsonRpcObject::VERSION
spec.authors = ["wbr"]
spec.email = ["jsonrpcobject@jamulii.com"]
spec.summary = "Simple ruby objects to manage json-rpc 2.0 data structures"
#spec.description = "TODO: Write a longer description or delete this line."
spec.homepage = "https://gist.github.com/ginjo/5b128c655e79e1b4e0e10c27a5098177"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.6.0"
#spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
#spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(__dir__) do
`git ls-files -z`.split("\x0").reject do |f|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
end
end
spec.bindir = "."
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["."]
# Uncomment to register a new dependency of your gem
# spec.add_dependency "example-gem", "~> 1.0"
# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html
end
require 'securerandom'
require 'json'
require_relative 'json_rpc_object_version'
#
# A simple class to build json-rpc request, notification, response, and error structures.
# This is a Gist on Github.
#
# https://gist.github.com/ginjo/5b128c655e79e1b4e0e10c27a5098177
#
# Automatically inserts IDs where appropriate.
# Provides methods to create a response/error object from a request object.
# Existing strings or hashes can be loaded into the class and use the above-mentioned methods.
#
class JsonRpcObject < Hash
ErrorCodes = {
'-32700' => ['Parse error', 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.'],
'-32600' => ['Invalid Request', 'The JSON sent is not a valid Request object.'],
'-32601' => ['Method not found', 'The method does not exist / is not available.'],
'-32602' => ['Invalid params', 'Invalid method parameter(s).'],
'-32603' => ['Internal error', 'Internal JSON-RPC error.'],
'-32000' => ['Server error', '-32000 to -32099 Reserved for implementation-defined server-errors.']
}
### CLASS METHODS ###
class << self
def common
self[{'jsonrpc' => "2.0"}]
end
# Creates a request object.
#
def request(method, params=nil)
common.merge(
'method' => method,
'params' => params,
'id' => SecureRandom.uuid
)
end
# Creates a notification object (no id).
#
def notification(method=nil, params=nil)
request(method, params).tap {|r| r.delete 'id'}
end
alias_method :notify, :notification
# Creates a response object (id is required).
#
def response(id, result=nil)
common.merge(
'result' => result,
'id' => id
)
end
# Creates an error object (code is required).
# TODO: What about id? (See instance method #error below).
#
def error(code, data=nil, message=nil)
out = common.merge(
'code' => code,
'message' => message,
'data' => data
)
err_obj = ErrorCodes[out['code']]
if err_obj
out['message'] = "#{err_obj[0]}" + (message ? " : #{message}" : '')
end
out
end
# Loads and returns a JsonRpc object from JsonRpc, Hash, or String.
# No validity checking is done.
#
def load(json_rpc)
if json_rpc.is_a?(String)
new.merge!(JSON.parse(json_rpc))
elsif json_rpc.is_a?(Hash)
new.merge!(load(json_rpc.to_json))
end
end
end # class methods
### INSTANCE METHODS ###
# Creates a response from a request object (automatic id attachment).
#
def response(result, include_original:true)
req = self.class.load(self)
out = req.class.response(req['id'], result)
if include_original
out['request'] = req
end
out
end
# Creates an error from a request object (automatic id attachment).
# Note that in ruby version < 3, if data is a hash, it will be interpreted
# as keyword args (and fail) unless message is also given.
# The solution is to always provide a message, even if it is nil.
#
def error(code, data=nil, message=nil, include_original:true)
req = self.class.load(self)
out = req.class.error(code, data, message)
out['id'] = req['id']
if include_original
out['request'] = req
end
out
end
end
# TODO: Consider using Hashie or hash-with-indiferent-access with this class.
# TODO: Create subclasses for Request, Response, Error, and Notification.
# TODO: What about error id?
# TODO: Change 'original' key name to 'request'.
class JsonRpcObject < Hash
VERSION = '0.0.1'
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment