lib/nanook/rpc.rb in nanook-2.5.1 vs lib/nanook/rpc.rb in nanook-3.0.0

- old
+ new

@@ -1,10 +1,11 @@ +# frozen_string_literal: true + require 'json' require 'symbolized' class Nanook - # The <tt>Nanook::Rpc</tt> class is responsible for maintaining the # connection to the RPC server, calling the RPC and parsing its response # into Ruby primitives. # # Internally, the {Nanook} class creates an instance of this class, and @@ -12,84 +13,143 @@ # instance of {Nanook#rpc} instead of by instantiating this class directly: # # nanook = Nanook.new # nanook.rpc(:accounts_create, wallet: wallet_id, count: 2) class Rpc - - # Default RPC server and port to connect to - DEFAULT_URI = "http://localhost:7076" - # Default request timeout in seconds + # Default RPC server and port to connect to. + DEFAULT_URI = 'http://[::1]:7076' + # Default request timeout in seconds. DEFAULT_TIMEOUT = 60 + # Error expected to be returned when the RPC makes a call that requires the + # `enable_control` setting to be enabled when it is disabled. + RPC_CONTROL_DISABLED_ERROR = 'RPC control is disabled' - def initialize(uri=DEFAULT_URI, timeout:DEFAULT_TIMEOUT) + def initialize(uri = DEFAULT_URI, timeout: DEFAULT_TIMEOUT) @rpc_server = URI(uri) - unless ['http', 'https'].include?(@rpc_server.scheme) - raise ArgumentError.new("URI must have http or https in it. Was given: #{uri}") + unless %w[http https].include?(@rpc_server.scheme) + raise ArgumentError, "URI must have http or https in it. Was given: #{uri}" end - @http = Net::HTTP.new(@rpc_server.host, @rpc_server.port) + @http = Net::HTTP.new(@rpc_server.hostname, @rpc_server.port) @http.read_timeout = timeout - @request = Net::HTTP::Post.new(@rpc_server.request_uri, {"user-agent" => "Ruby nanook gem"}) - @request.content_type = "application/json" + @request = Net::HTTP::Post.new(@rpc_server.request_uri, { 'user-agent' => "Ruby nanook gem v#{Nanook::VERSION}" }) + @request.content_type = 'application/json' end + # Tests the RPC connection. Returns +true+ if connection is successful, + # otherwise raises an exception. + # + # @raise [Errno::ECONNREFUSED] if connection is unsuccessful + # @return [Boolean] true if connection is successful + def test + call(:telemetry) + true + end + # Calls the RPC server and returns the response. # # @param action [Symbol] the "action" of the RPC to call. The RPC always # expects an "action" param to identify what RPC action is being called. # @param params [Hash] all other params to pass to the RPC # @return [Hash] the response from the RPC - def call(action, params={}) - # Stringify param values - params = Hash[params.map {|k, v| [k, v.to_s] }] + def call(action, params = {}) + coerce_to = params.delete(:_coerce) + access_as = params.delete(:_access) - @request.body = { action: action }.merge(params).to_json + raw_hash = make_call(action, params) - response = @http.request(@request) + check_for_errors!(raw_hash) - if response.is_a?(Net::HTTPSuccess) - hash = JSON.parse(response.body) - process_hash(hash) - else - raise Nanook::Error.new("Encountered net/http error #{response.code}: #{response.class.name}") - end + hash = parse_values(raw_hash) + + hash = hash[access_as] if access_as + hash = coerce_empty_string_to_type(hash, coerce_to) if coerce_to + + hash end # @return [String] - def inspect - "#{self.class.name}(host: \"#{@rpc_server}\", timeout: #{@http.read_timeout} object_id: \"#{"0x00%x" % (object_id << 1)}\")" + def to_s + "#{self.class.name}(host: \"#{@rpc_server}\", timeout: #{@http.read_timeout})" end + alias inspect to_s private + def make_call(action, params) + # Stringify param values + params = params.dup.transform_values do |v| + next v if v.is_a?(Array) + + v.to_s + end + + @request.body = { action: action }.merge(params).to_json + + response = @http.request(@request) + + raise Nanook::ConnectionError, "Encountered net/http error #{response.code}: #{response.class.name}" \ + unless response.is_a?(Net::HTTPSuccess) + + JSON.parse(response.body) + end + + # Raises a {Nanook::NodeRpcConfigurationError} or {Nanook::NodeRpcError} if the RPC + # response contains an `:error` key. + def check_for_errors!(response) + # Raise a special error for when `enable_control` should be enabled. + if response['error'] == RPC_CONTROL_DISABLED_ERROR + raise Nanook::NodeRpcConfigurationError, + 'RPC must have the `enable_control` setting enabled to perform this action.' + end + + # Raise any other error. + raise Nanook::NodeRpcError, "An error was returned from the RPC: #{response['error']}" if response.key?('error') + end + # Recursively parses the RPC response, sending values to #parse_value - def process_hash(h) - new_hash = h.map do |k,v| - v = if v.is_a?(Array) - if v[0].is_a?(Hash) - v.map{|v| process_hash(v)} - else - v.map{|v| parse_value(v)} - end - elsif v.is_a?(Hash) - process_hash(v) - else - parse_value(v) - end + def parse_values(hash) + new_hash = hash.map do |k, val| + new_val = case val + when Array + if val[0].is_a?(Hash) + val.map { |v| parse_values(v) } + else + val.map { |v| parse_value(v) } + end + when Hash + parse_values(val) + else + parse_value(val) + end - [k, v] + [k, new_val] end Hash[new_hash.sort].to_symbolized_hash end # Converts Strings to primitives - def parse_value(v) - return v.to_i if v.match(/^\d+\Z/) - return true if v == "true" - return false if v == "false" - v + def parse_value(value) + return value.to_i if value.match(/^\d+\Z/) + return true if value == 'true' + return false if value == 'false' + + value end + # Converts an empty String value into an empty version of another type. + # + # The RPC often returns an empty String as a value to signal + # emptiness, rather than consistent types like an empty Array, + # or empty Hash. + # + # @param response the value returned from the RPC server + # @param type the type to return an empty of + def coerce_empty_string_to_type(response, type) + return type.new if response == '' || response.nil? + + response + end end end