cosmos/lib/cosmos/io/json_rpc.rb
# encoding: ascii-8bit
# Copyright 2022 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# This program may also be used under the terms of a commercial or
# enterprise edition license of COSMOS if purchased from the
# copyright holder
require 'json'
require 'date'
class Object
def as_json(options = nil) #:nodoc:
if respond_to?(:to_hash)
to_hash
else
instance_variables
end
end
end
class Struct #:nodoc:
def as_json(options = nil)
pairs = []
self.each_pair { |k, v| pairs << k.to_s; pairs << v.as_json(options) }
Hash[*pairs]
end
end
class TrueClass
def as_json(options = nil) self end #:nodoc:
end
class FalseClass
def as_json(options = nil) self end #:nodoc:
end
class NilClass
def as_json(options = nil) self end #:nodoc:
end
class String
NON_ASCII_PRINTABLE = /[^\x21-\x7e\s]/
def as_json(options = nil)
if NON_ASCII_PRINTABLE.match?(self)
self.to_json_raw_object
else
self
end
end #:nodoc:
end
class Symbol
def as_json(options = nil) to_s end #:nodoc:
end
class Numeric
def as_json(options = nil) self end #:nodoc:
end
class Float
def self.json_create(object)
case object['raw']
when "Infinity" then Float::INFINITY
when "-Infinity" then -Float::INFINITY
when "NaN" then Float::NAN
end
end
def as_json(options = nil)
return { "json_class" => Float, "raw" => "Infinity" } if self.infinite? == 1
return { "json_class" => Float, "raw" => "-Infinity" } if self.infinite? == -1
return { "json_class" => Float, "raw" => "NaN" } if self.nan?
return self
end
end
class Regexp
def as_json(options = nil) to_s end #:nodoc:
end
module Enumerable
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
end
class Array
def as_json(options = nil) #:nodoc:
map { |v| v.as_json(options) }
end
end
class Hash
def as_json(options = nil) #:nodoc:
pairs = []
self.each { |k, v| pairs << k.to_s; pairs << v.as_json(options) }
Hash[*pairs]
end
end
class Time
def as_json(options = nil) #:nodoc:
to_json(options).remove_quotes
end
end
class Date
def as_json(options = nil) #:nodoc:
to_json(options).remove_quotes
end
end
class DateTime
def as_json(options = nil) #:nodoc:
to_json(options).remove_quotes
end
end
class Exception
def as_json(*a)
hash = {}
hash['class'] = self.class.name
hash['message'] = self.message
hash['backtrace'] = self.backtrace
instance_vars = {}
self.instance_variables.each do |instance_var_name|
instance_vars[instance_var_name.to_s] = self.instance_variable_get(instance_var_name.to_s.intern)
end
hash['instance_variables'] = instance_vars
hash.as_json(*a)
end
def to_json(*a)
as_json(*a).to_json(*a)
end
def self.from_hash(hash)
begin
# Get Error class handling namespaced constants
split_error_class_name = hash['class'].split("::")
error_class = Object
split_error_class_name.each do |name|
error_class = error_class.const_get(name)
end
rescue
error = Cosmos::JsonDRbUnknownError.new(hash['message'])
error.set_backtrace(hash['backtrace'].concat(caller()))
raise error
end
error = error_class.new(hash['message'])
error.set_backtrace(hash['backtrace'].concat(caller())) if hash['backtrace']
hash['instance_variables'].each do |name, value|
error.instance_variable_set(name.intern, value)
end
error
end
end
module Cosmos
# An unknown JSON DRb error which can be re-raised by Exception
class JsonDRbUnknownError < StandardError; end
# Base class for all JSON Remote Procedure Calls. Provides basic
# comparison and Hash to JSON conversions.
class JsonRpc
include Comparable
def initialize
@hash = {}
end
# @param other [JsonRpc] Another JsonRpc to compare hash values with
def <=>(other)
return nil unless other.respond_to?(:as_json)
self.as_json <=> other.as_json
end
# @param a [Array] Array of options
# @return [Hash] Hash representing the object
def as_json(*a)
@hash.as_json(*a)
end
# @param a [Array] Array of options
# @return [String] The JSON encoded String
def to_json(*a)
as_json(*a).to_json(*a)
end
end
# Represents a JSON Remote Procedure Call Request
class JsonRpcRequest < JsonRpc
DANGEROUS_METHODS = ['__send__', 'send', 'instance_eval', 'instance_exec']
# @param method_name [String] The name of the method to call
# @param method_params [Array<String>] Array of strings which represent the
# parameters to send to the method
# @param keyword_params [Hash<String, Variable>] Hash of key value keyword params
# @param id [Integer] The identifier which will be matched to the response
def initialize(method_name, method_params, keyword_params, id)
super()
@hash['jsonrpc'.freeze] = '2.0'.freeze
@hash['method'.freeze] = method_name.to_s
if method_params and method_params.length != 0
@hash['params'.freeze] = method_params
end
if keyword_params and keyword_params.length != 0
symbolized = {}
keyword_params.each do |key, value|
symbolized[key.intern] = value
end
@hash['keyword_params'.freeze] = symbolized
end
@hash['id'.freeze] = id.to_i
end
# @return [String] The method to call
def method
@hash['method'.freeze]
end
# @return [Array<String>] Array of strings which represent the
# parameters to send to the method
def params
@hash['params'.freeze] || []
end
# @return [Hash<String, Variable>] Hash which represents the
# keyword parameters to send to the method
def keyword_params
@hash['keyword_params'.freeze]
end
# @return [Integer] The request identifier
def id
@hash['id'.freeze]
end
# Creates a JsonRpcRequest object from a JSON encoded String. The version
# must be 2.0 and the JSON must include the method and id members.
#
# @param request_data [String] JSON encoded string representing the request
# @param request_headers [Hash] Request Header to include the auth token
# @return [JsonRpcRequest]
def self.from_json(request_data, request_headers)
hash = JSON.parse(request_data, :allow_nan => true, :create_additions => true)
hash['keyword_params']['token'] = request_headers['HTTP_AUTHORIZATION'] if request_headers['HTTP_AUTHORIZATION']
# Verify the jsonrpc version is correct and there is a method and id
raise unless hash['jsonrpc'.freeze] == "2.0".freeze && hash['method'.freeze] && hash['id'.freeze]
self.from_hash(hash)
rescue
raise "Invalid JSON-RPC 2.0 Request\n#{request_data.inspect}\n"
end
# Creates a JsonRpcRequest object from a Hash
#
# @param hash [Hash] Hash containing the following keys: method, params,
# and id
# @return [JsonRpcRequest]
def self.from_hash(hash)
self.new(hash['method'.freeze], hash['params'.freeze], hash['keyword_params'.freeze], hash['id'.freeze])
end
end
# Represents a JSON Remote Procedure Call Response
class JsonRpcResponse < JsonRpc
# @param id [Integer] The identifier which will be matched to the request
def initialize(id)
super()
@hash['jsonrpc'.freeze] = "2.0".freeze
@hash['id'.freeze] = id
end
# Creates a JsonRpcResponse object from a JSON encoded String. The version
# must be 2.0 and the JSON must include the id members. It must also
# include either result for success or error for failure but never both.
#
# @param response_data [String] JSON encoded string representing the response
# @return [JsonRpcResponse]
def self.from_json(response_data)
msg = "Invalid JSON-RPC 2.0 Response#{response_data.inspect}\n"
begin
hash = JSON.parse(response_data, :allow_nan => true, :create_additions => true)
rescue
raise $!, msg, $!.backtrace
end
# Verify the jsonrpc version is correct and there is an ID
raise msg unless hash['jsonrpc'.freeze] == "2.0".freeze and hash.key?('id'.freeze)
# If there is a result this is probably a good response
if hash.key?('result'.freeze)
# Can't have an error key in a good response
raise msg if hash.key?('error'.freeze)
JsonRpcSuccessResponse.from_hash(hash)
elsif hash.key?('error'.freeze)
# There was an error key so create an error response
JsonRpcErrorResponse.from_hash(hash)
else
# Neither a result or error key so raise exception
raise msg
end
end
end
# Represents a JSON Remote Procedure Call Success Response
class JsonRpcSuccessResponse < JsonRpcResponse
# @param id [Integer] The identifier which will be matched to the request
def initialize(result, id)
super(id)
@hash['result'.freeze] = result
end
# @return [Object] The result of the method request
def result
@hash['result'.freeze]
end
# Creates a JsonRpcSuccessResponse object from a Hash
#
# @param hash [Hash] Hash containing the following keys: result and id
# @return [JsonRpcSuccessResponse]
def self.from_hash(hash)
self.new(hash['result'.freeze], hash['id'.freeze])
end
end
# Represents a JSON Remote Procedure Call Error Response
class JsonRpcErrorResponse < JsonRpcResponse
# @param error [JsonRpcError] The error object
# @param id [Integer] The identifier which will be matched to the request
def initialize(error, id)
super(id)
@hash['error'.freeze] = error
end
# @return [JsonRpcError] The error object
def error
@hash['error'.freeze]
end
# Creates a JsonRpcErrorResponse object from a Hash
#
# @param hash [Hash] Hash containing the following keys: error and id
# @return [JsonRpcErrorResponse]
def self.from_hash(hash)
self.new(JsonRpcError.from_hash(hash['error'.freeze]), hash['id'.freeze])
end
end
# Represents a JSON Remote Procedure Call Error
class JsonRpcError < JsonRpc
# Enumeration of JSON RPC error codes
class ErrorCode
PARSE_ERROR = -32700
INVALID_REQUEST = -32600
METHOD_NOT_FOUND = -32601
INVALID_PARAMS = -32602
INTERNAL_ERROR = -32603
AUTH_ERROR = -32500
FORBIDDEN_ERROR = -32501
OTHER_ERROR = -1
end
# @param code [Integer] The error type that occurred
# @param message [String] A short description of the error
# @param data [Hash] Additional information about the error
def initialize(code, message, data = nil)
super()
@hash['code'] = code
@hash['message'] = message
@hash['data'] = data
end
# @return [Integer] The error type that occurred
def code
@hash['code']
end
# @return [String] A short description of the error
def message
@hash['message']
end
# @return [Hash] Additional information about the error
def data
@hash['data']
end
# Creates a JsonRpcError object from a Hash
#
# @param hash [Hash] Hash containing the following keys: code, message, and
# optionally data
# @return [JsonRpcError]
def self.from_hash(hash)
if hash['code'] and (hash['code'].to_i == hash['code']) and hash['message']
self.new(hash['code'], hash['message'], hash['data'])
else
raise "Invalid JSON-RPC 2.0 Error\n#{hash.inspect}\n"
end
end
end
end