├── VERSION ├── .gitignore ├── .travis.yml ├── lib ├── jimson.rb └── jimson │ ├── router.rb │ ├── request.rb │ ├── client │ └── error.rb │ ├── response.rb │ ├── handler.rb │ ├── server │ └── error.rb │ ├── router │ └── map.rb │ ├── client.rb │ └── server.rb ├── Gemfile ├── spec ├── spec_helper.rb ├── handler_spec.rb ├── router_spec.rb ├── client_spec.rb └── server_spec.rb ├── Rakefile ├── jimson.gemspec ├── README.md ├── Gemfile.lock ├── LICENSE.txt └── CHANGELOG.rdoc /VERSION: -------------------------------------------------------------------------------- 1 | 0.10.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | coverage 3 | .rvmrc 4 | *.swp 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.8.7 3 | - 1.9.2 4 | - 1.9.3 5 | - 2.0.0 6 | - jruby-18mode 7 | - jruby-19mode 8 | -------------------------------------------------------------------------------- /lib/jimson.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'jimson/handler' 3 | require 'jimson/router' 4 | require 'jimson/server' 5 | require 'jimson/client' 6 | 7 | module Jimson 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rspec' 6 | gem 'rack-test' 7 | gem 'rake' 8 | gem 'rdoc', '>= 2.4.2' 9 | 10 | platform :mri_18 do 11 | gem 'rcov' 12 | end 13 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | $:.unshift(File.dirname(__FILE__) + '/../lib/') 3 | require 'jimson' 4 | require 'bundler/setup' 5 | require 'multi_json' 6 | 7 | SPEC_URL = 'http://localhost:8999' 8 | -------------------------------------------------------------------------------- /lib/jimson/router.rb: -------------------------------------------------------------------------------- 1 | require 'jimson/router/map' 2 | require 'forwardable' 3 | 4 | module Jimson 5 | class Router 6 | extend Forwardable 7 | 8 | def_delegators :@map, :handler_for_method, 9 | :root, 10 | :namespace, 11 | :jimson_methods, 12 | :strip_method_namespace 13 | 14 | def initialize 15 | @map = Map.new 16 | end 17 | 18 | def draw(&block) 19 | @map.instance_eval &block 20 | self 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/jimson/request.rb: -------------------------------------------------------------------------------- 1 | module Jimson 2 | class Request 3 | 4 | attr_accessor :method, :params, :id 5 | def initialize(method, params, id = nil) 6 | @method = method 7 | @params = params 8 | @id = id 9 | end 10 | 11 | def to_h 12 | h = { 13 | 'jsonrpc' => '2.0', 14 | 'method' => @method 15 | } 16 | h.merge!('params' => @params) if !!@params && !params.empty? 17 | h.merge!('id' => id) 18 | end 19 | 20 | def to_json(*a) 21 | MultiJson.encode(self.to_h) 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/jimson/client/error.rb: -------------------------------------------------------------------------------- 1 | module Jimson 2 | class Client 3 | module Error 4 | class InvalidResponse < StandardError 5 | def initialize() 6 | super('Invalid or empty response from server.') 7 | end 8 | end 9 | 10 | class InvalidJSON < StandardError 11 | def initialize(json) 12 | super("Couldn't parse JSON string received from server:\n#{json}") 13 | end 14 | end 15 | 16 | class ServerError < StandardError 17 | def initialize(code, message) 18 | super("Server error #{code}: #{message}") 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rspec/core/rake_task' 4 | 5 | desc "Run all specs" 6 | RSpec::Core::RakeTask.new(:rspec) do |spec| 7 | spec.pattern = 'spec/**/*_spec.rb' 8 | end 9 | 10 | RSpec::Core::RakeTask.new(:rcov) do |spec| 11 | spec.pattern = 'spec/**/*_spec.rb' 12 | spec.rcov = true 13 | end 14 | 15 | task :default => :rspec 16 | 17 | require 'rdoc/task' 18 | 19 | Rake::RDocTask.new do |rdoc| 20 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 21 | 22 | rdoc.rdoc_dir = 'rdoc' 23 | rdoc.title = "jimson #{version}" 24 | rdoc.rdoc_files.include('README*') 25 | rdoc.rdoc_files.include('lib/**/*.rb') 26 | end 27 | -------------------------------------------------------------------------------- /lib/jimson/response.rb: -------------------------------------------------------------------------------- 1 | module Jimson 2 | class Response 3 | attr_accessor :result, :error, :id 4 | 5 | def initialize(id) 6 | @id = id 7 | end 8 | 9 | def to_h 10 | h = {'jsonrpc' => '2.0'} 11 | h.merge!('result' => @result) if !!@result 12 | h.merge!('error' => @error) if !!@error 13 | h.merge!('id' => @id) 14 | end 15 | 16 | def is_error? 17 | !!@error 18 | end 19 | 20 | def succeeded? 21 | !!@result 22 | end 23 | 24 | def populate!(data) 25 | @error = data['error'] if !!data['error'] 26 | @result = data['result'] if !!data['result'] 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/jimson/handler.rb: -------------------------------------------------------------------------------- 1 | module Jimson 2 | module Handler 3 | 4 | def jimson_default_methods 5 | self.instance_methods.map(&:to_s) - Object.methods.map(&:to_s) 6 | end 7 | 8 | def jimson_expose(*methods) 9 | @jimson_exposed_methods ||= [] 10 | @jimson_exposed_methods += methods.map(&:to_s) 11 | end 12 | 13 | def jimson_exclude(*methods) 14 | @jimson_excluded_methods ||= [] 15 | @jimson_excluded_methods += methods.map(&:to_s) 16 | end 17 | 18 | def jimson_exposed_methods 19 | @jimson_exposed_methods ||= [] 20 | @jimson_excluded_methods ||= [] 21 | (jimson_default_methods - @jimson_excluded_methods + @jimson_exposed_methods).sort 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /jimson.gemspec: -------------------------------------------------------------------------------- 1 | spec = Gem::Specification.new do |s| 2 | s.name = "jimson" 3 | s.version = "0.10.0" 4 | s.author = "Chris Kite" 5 | s.homepage = "http://www.github.com/chriskite/jimson" 6 | s.platform = Gem::Platform::RUBY 7 | s.summary = "JSON-RPC 2.0 client and server" 8 | s.require_path = "lib" 9 | s.has_rdoc = false 10 | #s.rdoc_options << '-m' << 'README.rdoc' << '-t' << 'Jimson' 11 | s.extra_rdoc_files = ["README.md"] 12 | s.add_dependency("blankslate", "~> 3.1.2") 13 | s.add_dependency("rest-client", "~> 1.6.7") 14 | s.add_dependency("multi_json", "~> 1.7.6") 15 | s.add_dependency("rack", "~> 1.4.5") 16 | 17 | s.files = %w[ 18 | VERSION 19 | LICENSE.txt 20 | CHANGELOG.rdoc 21 | README.md 22 | Rakefile 23 | ] + Dir['lib/**/*.rb'] 24 | 25 | s.test_files = Dir['spec/*.rb'] 26 | end 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jimson 2 | ### JSON-RPC 2.0 Client and Server for Ruby 3 | ![next build status](https://secure.travis-ci.org/chriskite/jimson.png?branch=next) 4 | 5 | ## Client: Quick Start 6 | require 'jimson' 7 | client = Jimson::Client.new("http://www.example.com:8999") # the URL for the JSON-RPC 2.0 server to connect to 8 | result = client.sum(1,2) # call the 'sum' method on the RPC server and save the result '3' 9 | 10 | ## Server: Quick Start 11 | require 'jimson' 12 | 13 | class MyHandler 14 | extend Jimson::Handler 15 | 16 | def sum(a,b) 17 | a + b 18 | end 19 | end 20 | 21 | server = Jimson::Server.new(MyHandler.new) 22 | server.start # serve with webrick on http://0.0.0.0:8999/ 23 | 24 | ## JSON Engine 25 | Jimson uses multi\_json, so you can load the JSON library of your choice in your application and Jimson will use it automatically. 26 | 27 | For example, require the 'json' gem in your application: 28 | require 'json' 29 | 30 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | jimson (0.9.1) 5 | blankslate (~> 3.1.2) 6 | multi_json (~> 1.7.6) 7 | rack (~> 1.4.5) 8 | rest-client (~> 1.6.7) 9 | 10 | GEM 11 | remote: http://rubygems.org/ 12 | specs: 13 | blankslate (3.1.2) 14 | diff-lcs (1.2.4) 15 | json (1.8.0) 16 | json (1.8.0-java) 17 | mime-types (1.23) 18 | multi_json (1.7.6) 19 | rack (1.4.5) 20 | rack-test (0.6.2) 21 | rack (>= 1.0) 22 | rake (10.0.4) 23 | rcov (1.0.0) 24 | rdoc (4.0.1) 25 | json (~> 1.4) 26 | rest-client (1.6.7) 27 | mime-types (>= 1.16) 28 | rspec (2.13.0) 29 | rspec-core (~> 2.13.0) 30 | rspec-expectations (~> 2.13.0) 31 | rspec-mocks (~> 2.13.0) 32 | rspec-core (2.13.1) 33 | rspec-expectations (2.13.0) 34 | diff-lcs (>= 1.1.3, < 2.0) 35 | rspec-mocks (2.13.1) 36 | 37 | PLATFORMS 38 | java 39 | ruby 40 | 41 | DEPENDENCIES 42 | jimson! 43 | rack-test 44 | rake 45 | rcov 46 | rdoc (>= 2.4.2) 47 | rspec 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /spec/handler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Jimson 4 | describe Handler do 5 | 6 | class FooHandler 7 | extend Jimson::Handler 8 | 9 | jimson_expose :to_s, :bye 10 | 11 | jimson_exclude :hi, :bye 12 | 13 | def hi 14 | 'hi' 15 | end 16 | 17 | def bye 18 | 'bye' 19 | end 20 | 21 | def to_s 22 | 'foo' 23 | end 24 | 25 | def so_exposed 26 | "I'm so exposed!" 27 | end 28 | end 29 | 30 | let(:foo) { FooHandler.new } 31 | 32 | describe "#jimson_expose" do 33 | it "exposes a method even if it was defined on Object" do 34 | foo.class.jimson_exposed_methods.should include('to_s') 35 | end 36 | end 37 | 38 | describe "#jimson_exclude" do 39 | context "when a method was not explicitly exposed" do 40 | it "excludes the method" do 41 | foo.class.jimson_exposed_methods.should_not include('hi') 42 | end 43 | end 44 | context "when a method was explicitly exposed" do 45 | it "does not exclude the method" do 46 | foo.class.jimson_exposed_methods.should include('bye') 47 | end 48 | end 49 | end 50 | 51 | describe "#jimson_exposed_methods" do 52 | it "doesn't include methods defined on Object" do 53 | foo.class.jimson_exposed_methods.should_not include('object_id') 54 | end 55 | it "includes methods defined on the extending class but not on Object" do 56 | foo.class.jimson_exposed_methods.should include('so_exposed') 57 | end 58 | end 59 | 60 | end 61 | end 62 | 63 | -------------------------------------------------------------------------------- /lib/jimson/server/error.rb: -------------------------------------------------------------------------------- 1 | module Jimson 2 | class Server 3 | class Error < StandardError 4 | attr_accessor :code, :message 5 | 6 | def initialize(code, message) 7 | @code = code 8 | @message = message 9 | super(message) 10 | end 11 | 12 | def to_h 13 | { 14 | 'code' => @code, 15 | 'message' => @message 16 | } 17 | end 18 | 19 | class ParseError < Error 20 | def initialize 21 | super(-32700, 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.') 22 | end 23 | end 24 | 25 | class InvalidRequest < Error 26 | def initialize 27 | super(-32600, 'The JSON sent is not a valid Request object.') 28 | end 29 | end 30 | 31 | class MethodNotFound < Error 32 | def initialize(method) 33 | super(-32601, "Method '#{method}' not found.") 34 | end 35 | end 36 | 37 | class InvalidParams < Error 38 | def initialize 39 | super(-32602, 'Invalid method parameter(s).') 40 | end 41 | end 42 | 43 | class InternalError < Error 44 | def initialize(e) 45 | super(-32603, "Internal server error: #{e}") 46 | end 47 | end 48 | 49 | class ApplicationError < Error 50 | def initialize(err, show_error = false) 51 | msg = "Server application error" 52 | msg += ': ' + err.message + ' at ' + err.backtrace.first if show_error 53 | super(-32099, msg) 54 | end 55 | end 56 | 57 | CODES = { 58 | -32700 => ParseError, 59 | -32600 => InvalidRequest, 60 | -32601 => MethodNotFound, 61 | -32602 => InvalidParams, 62 | -32603 => InternalError 63 | } 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/router_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Jimson 4 | describe Router do 5 | 6 | let(:router) { Router.new } 7 | 8 | class RouterFooHandler 9 | extend Jimson::Handler 10 | 11 | def hi 12 | 'hi' 13 | end 14 | end 15 | 16 | class RouterBarHandler 17 | extend Jimson::Handler 18 | 19 | def bye 20 | 'bye' 21 | end 22 | end 23 | 24 | class RouterBazHandler 25 | extend Jimson::Handler 26 | 27 | def meh 28 | 'mehkayla' 29 | end 30 | end 31 | 32 | 33 | describe '#draw' do 34 | context 'when given non-nested namespaces' do 35 | it 'takes a block with a DSL to set the root and namespaces' do 36 | router.draw do 37 | root RouterFooHandler 38 | namespace 'ns', RouterBarHandler 39 | end 40 | 41 | router.handler_for_method('hi').should be_a(RouterFooHandler) 42 | router.handler_for_method('ns.hi').should be_a(RouterBarHandler) 43 | end 44 | end 45 | 46 | context 'when given nested namespaces' do 47 | it 'takes a block with a DSL to set the root and namespaces' do 48 | router.draw do 49 | root RouterFooHandler 50 | namespace 'ns1' do 51 | root RouterBazHandler 52 | namespace 'ns2', RouterBarHandler 53 | end 54 | end 55 | 56 | router.handler_for_method('hi').should be_a(RouterFooHandler) 57 | router.handler_for_method('ns1.hi').should be_a(RouterBazHandler) 58 | router.handler_for_method('ns1.ns2.hi').should be_a(RouterBarHandler) 59 | end 60 | end 61 | end 62 | 63 | describe '#jimson_methods' do 64 | it 'returns an array of namespaced method names from all registered handlers' do 65 | router.draw do 66 | root RouterFooHandler 67 | namespace 'foo', RouterBarHandler 68 | end 69 | 70 | router.jimson_methods.sort.should == ['hi', 'foo.bye'].sort 71 | end 72 | end 73 | 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /CHANGELOG.rdoc: -------------------------------------------------------------------------------- 1 | == 0.10.0 / 2013-06-28 2 | 3 | * Minor enhancements 4 | 5 | * Update dependency versions 6 | 7 | == 0.9.1 / 2012-09-18 8 | 9 | * Bug fixes 10 | 11 | * Allow opts to be passed to Server.with_routes 12 | 13 | == 0.9.0 / 2012-08-22 14 | 15 | * Minor enhancements 16 | 17 | * Add show_errors option to server, which will cause application errors to include the error name and the first line of the backtrace 18 | 19 | == 0.8.0 / 2012-08-17 20 | 21 | * Major enhancements 22 | 23 | * Add namespaced method calls to client (e.g. 'client[:foo].sum(1,2,3) # calls foo.sum') 24 | * Add Server.with_routes to quickly created a routed server 25 | 26 | == 0.7.1 / 2012-08-16 27 | 28 | * Bug fixes 29 | 30 | * Fix handling of array params in client, which were erroneously being flattened 31 | 32 | == 0.7.0 / 2012-04-13 33 | 34 | * Major enhancements 35 | 36 | * Add namespaced routing 37 | 38 | * Bug fixes 39 | 40 | * Fix deprecation warning about RDoc task in Rakefile 41 | 42 | == 0.6.0 / 2012-03-14 43 | 44 | * Minor enhancements 45 | 46 | * Add ability to pass options to Rack and RestClient 47 | 48 | == 0.5.0 / 2012-03-06 49 | 50 | * Major enhancements 51 | 52 | * Switch to MultiJson from json gem 53 | 54 | * Bug fixes 55 | 56 | * Allow BigNum in 'id' field of request and response 57 | 58 | == 0.3.1 / 2011-08-11 59 | 60 | * Minor enhancements 61 | 62 | * Refactor the way the server is intantiated/started to work better with config.ru 63 | 64 | == 0.3.0 / 2011-08-11 65 | 66 | * Major enhancements 67 | 68 | * Replace eventmachine-httpserver with rack for more cross-platform goodness 69 | 70 | == 0.2.3 / 2011-08-01 71 | 72 | * Bug fixes 73 | 74 | * Fix argument error in client error handling 75 | 76 | == 0.2.2 / 2011-07-28 77 | 78 | * Bug fixes 79 | 80 | * Fix invalid local variable error in client error handling 81 | 82 | == 0.2.1 / 2011-07-27 83 | 84 | * Bug fixes 85 | 86 | * Fix error in client handling some errors caused by errant 'new' keyword 87 | 88 | == 0.2.0 / 2011-07-20 89 | 90 | * Major enhancements 91 | 92 | * Replace patron with rest-client for JRuby compatibility in the client 93 | -------------------------------------------------------------------------------- /lib/jimson/router/map.rb: -------------------------------------------------------------------------------- 1 | module Jimson 2 | class Router 3 | 4 | # 5 | # Provides a DSL for routing method namespaces to handlers. 6 | # Only handles root-level and non-nested namespaces, e.g. 'foo.bar' or 'foo'. 7 | # 8 | class Map 9 | 10 | def initialize 11 | @routes = {} 12 | end 13 | 14 | # 15 | # Set the root handler, i.e. the handler used for a bare method like 'foo' 16 | # 17 | def root(handler) 18 | handler = handler.new if handler.is_a?(Class) 19 | @routes[''] = handler 20 | end 21 | 22 | # 23 | # Define the handler for a namespace 24 | # 25 | def namespace(ns, handler = nil, &block) 26 | if !!handler 27 | handler = handler.new if handler.is_a?(Class) 28 | @routes[ns.to_s] = handler 29 | else 30 | # passed a block for nested namespacing 31 | map = Jimson::Router::Map.new 32 | @routes[ns.to_s] = map 33 | map.instance_eval &block 34 | end 35 | end 36 | 37 | # 38 | # Return the handler for a (possibly namespaced) method name 39 | # 40 | def handler_for_method(method) 41 | parts = method.split('.') 42 | ns = (method.index('.') == nil ? '' : parts.first) 43 | handler = @routes[ns] 44 | if handler.is_a?(Jimson::Router::Map) 45 | return handler.handler_for_method(parts[1..-1].join('.')) 46 | end 47 | handler 48 | end 49 | 50 | # 51 | # Strip off the namespace part of a method and return the bare method name 52 | # 53 | def strip_method_namespace(method) 54 | method.split('.').last 55 | end 56 | 57 | # 58 | # Return an array of all methods on handlers in the map, fully namespaced 59 | # 60 | def jimson_methods 61 | arr = @routes.keys.map do |ns| 62 | prefix = (ns == '' ? '' : "#{ns}.") 63 | handler = @routes[ns] 64 | if handler.is_a?(Jimson::Router::Map) 65 | handler.jimson_methods 66 | else 67 | handler.class.jimson_exposed_methods.map { |method| prefix + method } 68 | end 69 | end 70 | arr.flatten 71 | end 72 | 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/jimson/client.rb: -------------------------------------------------------------------------------- 1 | require 'blankslate' 2 | require 'multi_json' 3 | require 'rest-client' 4 | require 'jimson/request' 5 | require 'jimson/response' 6 | 7 | module Jimson 8 | class ClientHelper 9 | JSON_RPC_VERSION = '2.0' 10 | 11 | def self.make_id 12 | rand(10**12) 13 | end 14 | 15 | def initialize(url, opts = {}, namespace = nil) 16 | @url = url 17 | URI.parse(@url) # for the sake of validating the url 18 | @batch = [] 19 | @opts = opts 20 | @namespace = namespace 21 | @opts[:content_type] = 'application/json' 22 | end 23 | 24 | def process_call(sym, args) 25 | resp = send_single_request(sym.to_s, args) 26 | 27 | begin 28 | data = MultiJson.decode(resp) 29 | rescue 30 | raise Client::Error::InvalidJSON.new(resp) 31 | end 32 | 33 | return process_single_response(data) 34 | 35 | rescue Exception, StandardError => e 36 | e.extend(Client::Error) unless e.is_a?(Client::Error) 37 | raise e 38 | end 39 | 40 | def send_single_request(method, args) 41 | namespaced_method = @namespace.nil? ? method : "#@namespace#{method}" 42 | post_data = MultiJson.encode({ 43 | 'jsonrpc' => JSON_RPC_VERSION, 44 | 'method' => namespaced_method, 45 | 'params' => args, 46 | 'id' => self.class.make_id 47 | }) 48 | resp = RestClient.post(@url, post_data, @opts) 49 | if resp.nil? || resp.body.nil? || resp.body.empty? 50 | raise Client::Error::InvalidResponse.new 51 | end 52 | 53 | return resp.body 54 | end 55 | 56 | def send_batch_request(batch) 57 | post_data = MultiJson.encode(batch) 58 | resp = RestClient.post(@url, post_data, @opts) 59 | if resp.nil? || resp.body.nil? || resp.body.empty? 60 | raise Client::Error::InvalidResponse.new 61 | end 62 | 63 | return resp.body 64 | end 65 | 66 | def process_batch_response(responses) 67 | responses.each do |resp| 68 | saved_response = @batch.map { |r| r[1] }.select { |r| r.id == resp['id'] }.first 69 | raise Client::Error::InvalidResponse.new if saved_response.nil? 70 | saved_response.populate!(resp) 71 | end 72 | end 73 | 74 | def process_single_response(data) 75 | raise Client::Error::InvalidResponse.new if !valid_response?(data) 76 | 77 | if !!data['error'] 78 | code = data['error']['code'] 79 | msg = data['error']['message'] 80 | raise Client::Error::ServerError.new(code, msg) 81 | end 82 | 83 | return data['result'] 84 | end 85 | 86 | def valid_response?(data) 87 | return false if !data.is_a?(Hash) 88 | 89 | return false if data['jsonrpc'] != JSON_RPC_VERSION 90 | 91 | return false if !data.has_key?('id') 92 | 93 | return false if data.has_key?('error') && data.has_key?('result') 94 | 95 | if data.has_key?('error') 96 | if !data['error'].is_a?(Hash) || !data['error'].has_key?('code') || !data['error'].has_key?('message') 97 | return false 98 | end 99 | 100 | if !data['error']['code'].is_a?(Fixnum) || !data['error']['message'].is_a?(String) 101 | return false 102 | end 103 | end 104 | 105 | return true 106 | 107 | rescue 108 | return false 109 | end 110 | 111 | def push_batch_request(request) 112 | request.id = self.class.make_id 113 | response = Response.new(request.id) 114 | @batch << [request, response] 115 | return response 116 | end 117 | 118 | def send_batch 119 | batch = @batch.map(&:first) # get the requests 120 | response = send_batch_request(batch) 121 | 122 | begin 123 | responses = MultiJson.decode(response) 124 | rescue 125 | raise Client::Error::InvalidJSON.new(json) 126 | end 127 | 128 | process_batch_response(responses) 129 | @batch = [] 130 | end 131 | 132 | end 133 | 134 | class BatchClient < BlankSlate 135 | 136 | def initialize(helper) 137 | @helper = helper 138 | end 139 | 140 | def method_missing(sym, *args, &block) 141 | request = Jimson::Request.new(sym.to_s, args) 142 | @helper.push_batch_request(request) 143 | end 144 | 145 | end 146 | 147 | class Client < BlankSlate 148 | reveal :instance_variable_get 149 | reveal :inspect 150 | reveal :to_s 151 | 152 | def self.batch(client) 153 | helper = client.instance_variable_get(:@helper) 154 | batch_client = BatchClient.new(helper) 155 | yield batch_client 156 | helper.send_batch 157 | end 158 | 159 | def initialize(url, opts = {}, namespace = nil) 160 | @url, @opts, @namespace = url, opts, namespace 161 | @helper = ClientHelper.new(url, opts, namespace) 162 | end 163 | 164 | def method_missing(sym, *args, &block) 165 | @helper.process_call(sym, args) 166 | end 167 | 168 | def [](method, *args) 169 | if method.is_a?(Symbol) 170 | # namespace requested 171 | new_ns = @namespace.nil? ? "#{method}." : "#@namespace#{method}." 172 | return Client.new(@url, @opts, new_ns) 173 | end 174 | @helper.process_call(method, args) 175 | end 176 | 177 | end 178 | end 179 | 180 | require 'jimson/client/error' 181 | -------------------------------------------------------------------------------- /lib/jimson/server.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'rack/request' 3 | require 'rack/response' 4 | require 'multi_json' 5 | require 'jimson/handler' 6 | require 'jimson/router' 7 | require 'jimson/server/error' 8 | 9 | module Jimson 10 | class Server 11 | 12 | class System 13 | extend Handler 14 | 15 | def initialize(router) 16 | @router = router 17 | end 18 | 19 | def listMethods 20 | @router.jimson_methods 21 | end 22 | 23 | def isAlive 24 | true 25 | end 26 | end 27 | 28 | JSON_RPC_VERSION = '2.0' 29 | 30 | attr_accessor :router, :host, :port, :show_errors, :opts 31 | 32 | # 33 | # Create a Server with routes defined 34 | # 35 | def self.with_routes(opts = {}, &block) 36 | router = Router.new 37 | router.send(:draw, &block) 38 | self.new(router, opts) 39 | end 40 | 41 | # 42 | # +router_or_handler+ is an instance of Jimson::Router or extends Jimson::Handler 43 | # 44 | # +opts+ may include: 45 | # * :host - the hostname or ip to bind to 46 | # * :port - the port to listen on 47 | # * :server - the rack handler to use, e.g. 'webrick' or 'thin' 48 | # * :show_errors - true or false, send backtraces in error responses? 49 | # 50 | # Remaining options are forwarded to the underlying Rack server. 51 | # 52 | def initialize(router_or_handler, opts = {}) 53 | if !router_or_handler.is_a?(Router) 54 | # arg is a handler, wrap it in a Router 55 | @router = Router.new 56 | @router.root router_or_handler 57 | else 58 | # arg is a router 59 | @router = router_or_handler 60 | end 61 | @router.namespace 'system', System.new(@router) 62 | 63 | @host = opts.delete(:host) || '0.0.0.0' 64 | @port = opts.delete(:port) || 8999 65 | @show_errors = opts.delete(:show_errors) || false 66 | @opts = opts 67 | end 68 | 69 | # 70 | # Starts the server so it can process requests 71 | # 72 | def start 73 | Rack::Server.start(opts.merge( 74 | :app => self, 75 | :Host => @host, 76 | :Port => @port 77 | )) 78 | end 79 | 80 | # 81 | # Entry point for Rack 82 | # 83 | def call(env) 84 | req = Rack::Request.new(env) 85 | resp = Rack::Response.new 86 | return resp.finish if !req.post? 87 | resp.write process(req.body.read) 88 | resp.finish 89 | end 90 | 91 | def process(content) 92 | begin 93 | request = parse_request(content) 94 | if request.is_a?(Array) 95 | raise Server::Error::InvalidRequest.new if request.empty? 96 | response = request.map { |req| handle_request(req) } 97 | else 98 | response = handle_request(request) 99 | end 100 | rescue Server::Error::ParseError, Server::Error::InvalidRequest => e 101 | response = error_response(e) 102 | rescue Server::Error => e 103 | response = error_response(e, request) 104 | rescue StandardError, Exception => e 105 | response = error_response(Server::Error::InternalError.new(e)) 106 | end 107 | 108 | response.compact! if response.is_a?(Array) 109 | 110 | return nil if response.nil? || (response.respond_to?(:empty?) && response.empty?) 111 | 112 | MultiJson.encode(response) 113 | end 114 | 115 | def handle_request(request) 116 | response = nil 117 | begin 118 | if !validate_request(request) 119 | response = error_response(Server::Error::InvalidRequest.new) 120 | else 121 | response = create_response(request) 122 | end 123 | rescue Server::Error => e 124 | response = error_response(e, request) 125 | end 126 | 127 | response 128 | end 129 | 130 | def validate_request(request) 131 | required_keys = %w(jsonrpc method) 132 | required_types = { 133 | 'jsonrpc' => [String], 134 | 'method' => [String], 135 | 'params' => [Hash, Array], 136 | 'id' => [String, Fixnum, Bignum, NilClass] 137 | } 138 | 139 | return false if !request.is_a?(Hash) 140 | 141 | required_keys.each do |key| 142 | return false if !request.has_key?(key) 143 | end 144 | 145 | required_types.each do |key, types| 146 | return false if request.has_key?(key) && !types.any? { |type| request[key].is_a?(type) } 147 | end 148 | 149 | return false if request['jsonrpc'] != JSON_RPC_VERSION 150 | 151 | true 152 | end 153 | 154 | def create_response(request) 155 | method = request['method'] 156 | params = request['params'] 157 | result = dispatch_request(method, params) 158 | 159 | response = success_response(request, result) 160 | 161 | # A Notification is a Request object without an "id" member. 162 | # The Server MUST NOT reply to a Notification, including those 163 | # that are within a batch request. 164 | response = nil if !request.has_key?('id') 165 | 166 | return response 167 | 168 | rescue Server::Error => e 169 | raise e 170 | rescue ArgumentError 171 | raise Server::Error::InvalidParams.new 172 | rescue Exception, StandardError => e 173 | raise Server::Error::ApplicationError.new(e, @show_errors) 174 | end 175 | 176 | def dispatch_request(method, params) 177 | method_name = method.to_s 178 | handler = @router.handler_for_method(method_name) 179 | method_name = @router.strip_method_namespace(method_name) 180 | 181 | if handler.nil? \ 182 | || !handler.class.jimson_exposed_methods.include?(method_name) \ 183 | || !handler.respond_to?(method_name) 184 | raise Server::Error::MethodNotFound.new(method) 185 | end 186 | 187 | if params.nil? 188 | return handler.send(method_name) 189 | elsif params.is_a?(Hash) 190 | return handler.send(method_name, params) 191 | else 192 | return handler.send(method_name, *params) 193 | end 194 | end 195 | 196 | def error_response(error, request = nil) 197 | resp = { 198 | 'jsonrpc' => JSON_RPC_VERSION, 199 | 'error' => error.to_h, 200 | } 201 | if !!request && request.has_key?('id') 202 | resp['id'] = request['id'] 203 | else 204 | resp['id'] = nil 205 | end 206 | 207 | resp 208 | end 209 | 210 | def success_response(request, result) 211 | { 212 | 'jsonrpc' => JSON_RPC_VERSION, 213 | 'result' => result, 214 | 'id' => request['id'] 215 | } 216 | end 217 | 218 | def parse_request(post) 219 | data = MultiJson.decode(post) 220 | rescue 221 | raise Server::Error::ParseError.new 222 | end 223 | 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /spec/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Jimson 4 | describe Client do 5 | BOILERPLATE = {'jsonrpc' => '2.0', 'id' => 1} 6 | 7 | before(:each) do 8 | @resp_mock = mock('http_response') 9 | ClientHelper.stub!(:make_id).and_return(1) 10 | end 11 | 12 | after(:each) do 13 | end 14 | 15 | describe "hidden methods" do 16 | it "should reveal inspect" do 17 | Client.new(SPEC_URL).inspect.should match /Jimson::Client/ 18 | end 19 | 20 | it "should reveal to_s" do 21 | Client.new(SPEC_URL).to_s.should match /Jimson::Client/ 22 | end 23 | end 24 | 25 | describe "#[]" do 26 | before(:each) do 27 | @client = Client.new(SPEC_URL) 28 | end 29 | 30 | context "when using a symbol to specify a namespace" do 31 | it "sends the method prefixed with the namespace in the request" do 32 | expected = MultiJson.encode({ 33 | 'jsonrpc' => '2.0', 34 | 'method' => 'foo.sum', 35 | 'params' => [1,2,3], 36 | 'id' => 1 37 | }) 38 | response = MultiJson.encode(BOILERPLATE.merge({'result' => 42})) 39 | RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock) 40 | @resp_mock.should_receive(:body).at_least(:once).and_return(response) 41 | @client[:foo].sum(1, 2, 3).should == 42 42 | end 43 | 44 | context "when the namespace is nested" do 45 | it "sends the method prefixed with the full namespace in the request" do 46 | expected = MultiJson.encode({ 47 | 'jsonrpc' => '2.0', 48 | 'method' => 'foo.bar.sum', 49 | 'params' => [1,2,3], 50 | 'id' => 1 51 | }) 52 | response = MultiJson.encode(BOILERPLATE.merge({'result' => 42})) 53 | RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock) 54 | @resp_mock.should_receive(:body).at_least(:once).and_return(response) 55 | @client[:foo][:bar].sum(1, 2, 3).should == 42 56 | end 57 | end 58 | end 59 | 60 | context "when sending positional arguments" do 61 | it "sends a request with the correct method and args" do 62 | expected = MultiJson.encode({ 63 | 'jsonrpc' => '2.0', 64 | 'method' => 'foo', 65 | 'params' => [1,2,3], 66 | 'id' => 1 67 | }) 68 | response = MultiJson.encode(BOILERPLATE.merge({'result' => 42})) 69 | RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock) 70 | @resp_mock.should_receive(:body).at_least(:once).and_return(response) 71 | @client['foo', 1, 2, 3].should == 42 72 | end 73 | 74 | context "when one of the args is an array" do 75 | it "sends a request with the correct method and args" do 76 | expected = MultiJson.encode({ 77 | 'jsonrpc' => '2.0', 78 | 'method' => 'foo', 79 | 'params' => [[1,2],3], 80 | 'id' => 1 81 | }) 82 | response = MultiJson.encode(BOILERPLATE.merge({'result' => 42})) 83 | RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock) 84 | @resp_mock.should_receive(:body).at_least(:once).and_return(response) 85 | @client['foo', [1, 2], 3].should == 42 86 | end 87 | end 88 | end 89 | end 90 | 91 | describe "sending a single request" do 92 | context "when using positional parameters" do 93 | before(:each) do 94 | @expected = MultiJson.encode({ 95 | 'jsonrpc' => '2.0', 96 | 'method' => 'foo', 97 | 'params' => [1,2,3], 98 | 'id' => 1 99 | }) 100 | end 101 | it "sends a valid JSON-RPC request and returns the result" do 102 | response = MultiJson.encode(BOILERPLATE.merge({'result' => 42})) 103 | RestClient.should_receive(:post).with(SPEC_URL, @expected, {:content_type => 'application/json'}).and_return(@resp_mock) 104 | @resp_mock.should_receive(:body).at_least(:once).and_return(response) 105 | client = Client.new(SPEC_URL) 106 | client.foo(1,2,3).should == 42 107 | end 108 | 109 | it "sends a valid JSON-RPC request with custom options" do 110 | response = MultiJson.encode(BOILERPLATE.merge({'result' => 42})) 111 | RestClient.should_receive(:post).with(SPEC_URL, @expected, {:content_type => 'application/json', :timeout => 10000}).and_return(@resp_mock) 112 | @resp_mock.should_receive(:body).at_least(:once).and_return(response) 113 | client = Client.new(SPEC_URL, :timeout => 10000) 114 | client.foo(1,2,3).should == 42 115 | end 116 | end 117 | 118 | context "when one of the parameters is an array" do 119 | it "sends a correct JSON-RPC request (array is preserved) and returns the result" do 120 | expected = MultiJson.encode({ 121 | 'jsonrpc' => '2.0', 122 | 'method' => 'foo', 123 | 'params' => [[1,2],3], 124 | 'id' => 1 125 | }) 126 | response = MultiJson.encode(BOILERPLATE.merge({'result' => 42})) 127 | RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock) 128 | @resp_mock.should_receive(:body).at_least(:once).and_return(response) 129 | client = Client.new(SPEC_URL) 130 | client.foo([1,2],3).should == 42 131 | end 132 | end 133 | end 134 | 135 | describe "sending a batch request" do 136 | it "sends a valid JSON-RPC batch request and puts the results in the response objects" do 137 | batch = MultiJson.encode([ 138 | {"jsonrpc" => "2.0", "method" => "sum", "params" => [1,2,4], "id" => "1"}, 139 | {"jsonrpc" => "2.0", "method" => "subtract", "params" => [42,23], "id" => "2"}, 140 | {"jsonrpc" => "2.0", "method" => "foo_get", "params" => [{"name" => "myself"}], "id" => "5"}, 141 | {"jsonrpc" => "2.0", "method" => "get_data", "id" => "9"} 142 | ]) 143 | 144 | response = MultiJson.encode([ 145 | {"jsonrpc" => "2.0", "result" => 7, "id" => "1"}, 146 | {"jsonrpc" => "2.0", "result" => 19, "id" => "2"}, 147 | {"jsonrpc" => "2.0", "error" => {"code" => -32601, "message" => "Method not found."}, "id" => "5"}, 148 | {"jsonrpc" => "2.0", "result" => ["hello", 5], "id" => "9"} 149 | ]) 150 | 151 | ClientHelper.stub!(:make_id).and_return('1', '2', '5', '9') 152 | RestClient.should_receive(:post).with(SPEC_URL, batch, {:content_type => 'application/json'}).and_return(@resp_mock) 153 | @resp_mock.should_receive(:body).at_least(:once).and_return(response) 154 | client = Client.new(SPEC_URL) 155 | 156 | sum = subtract = foo = data = nil 157 | Jimson::Client.batch(client) do |batch| 158 | sum = batch.sum(1,2,4) 159 | subtract = batch.subtract(42,23) 160 | foo = batch.foo_get('name' => 'myself') 161 | data = batch.get_data 162 | end 163 | 164 | sum.succeeded?.should be_true 165 | sum.is_error?.should be_false 166 | sum.result.should == 7 167 | 168 | subtract.result.should == 19 169 | 170 | foo.is_error?.should be_true 171 | foo.succeeded?.should be_false 172 | foo.error['code'].should == -32601 173 | 174 | data.result.should == ['hello', 5] 175 | end 176 | end 177 | 178 | describe "error handling" do 179 | context "when an error occurs in the Jimson::Client code" do 180 | it "tags the raised exception with Jimson::Client::Error" do 181 | client_helper = ClientHelper.new(SPEC_URL) 182 | ClientHelper.stub!(:new).and_return(client_helper) 183 | client = Client.new(SPEC_URL) 184 | client_helper.stub!(:send_single_request).and_raise "intentional error" 185 | lambda { client.foo }.should raise_error(Jimson::Client::Error) 186 | end 187 | end 188 | end 189 | 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /spec/server_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rack/test' 3 | 4 | module Jimson 5 | describe Server do 6 | include Rack::Test::Methods 7 | 8 | class TestHandler 9 | extend Jimson::Handler 10 | 11 | def subtract(a, b = nil) 12 | if a.is_a?(Hash) 13 | return a['minuend'] - a['subtrahend'] 14 | else 15 | return a - b 16 | end 17 | end 18 | 19 | def sum(a,b,c) 20 | a + b + c 21 | end 22 | 23 | def car(array) 24 | array.first 25 | end 26 | 27 | def notify_hello(*args) 28 | # notification, doesn't do anything 29 | end 30 | 31 | def update(*args) 32 | # notification, doesn't do anything 33 | end 34 | 35 | def get_data 36 | ['hello', 5] 37 | end 38 | 39 | def ugly_method 40 | raise RuntimeError 41 | end 42 | end 43 | 44 | class OtherHandler 45 | extend Jimson::Handler 46 | 47 | def multiply(a,b) 48 | a * b 49 | end 50 | end 51 | 52 | INVALID_RESPONSE_EXPECTATION = { 53 | 'jsonrpc' => '2.0', 54 | 'error' => { 55 | 'code' => -32600, 56 | 'message' => 'The JSON sent is not a valid Request object.' 57 | }, 58 | 'id' => nil 59 | } 60 | let(:router) do 61 | router = Router.new.draw do 62 | root TestHandler.new 63 | namespace 'other', OtherHandler.new 64 | end 65 | end 66 | 67 | let(:app) do 68 | Server.new(router, :environment => "production") 69 | end 70 | 71 | def post_json(hash) 72 | post '/', MultiJson.encode(hash), {'Content-Type' => 'application/json'} 73 | end 74 | 75 | before(:each) do 76 | @url = SPEC_URL 77 | end 78 | 79 | it "exposes the given options" do 80 | app.opts.should == { :environment => "production" } 81 | end 82 | 83 | describe "receiving a request with positional parameters" do 84 | context "when no errors occur" do 85 | it "returns a response with 'result'" do 86 | req = { 87 | 'jsonrpc' => '2.0', 88 | 'method' => 'subtract', 89 | 'params' => [24, 20], 90 | 'id' => 1 91 | } 92 | post_json(req) 93 | 94 | last_response.should be_ok 95 | resp = MultiJson.decode(last_response.body) 96 | resp.should == { 97 | 'jsonrpc' => '2.0', 98 | 'result' => 4, 99 | 'id' => 1 100 | } 101 | end 102 | 103 | it "handles an array in the parameters" do 104 | req = { 105 | 'jsonrpc' => '2.0', 106 | 'method' => 'car', 107 | 'params' => [['a', 'b']], 108 | 'id' => 1 109 | } 110 | post_json(req) 111 | 112 | last_response.should be_ok 113 | resp = MultiJson.decode(last_response.body) 114 | resp.should == { 115 | 'jsonrpc' => '2.0', 116 | 'result' => 'a', 117 | 'id' => 1 118 | } 119 | end 120 | 121 | it "handles bignums" do 122 | req = { 123 | 'jsonrpc' => '2.0', 124 | 'method' => 'subtract', 125 | 'params' => [24, 20], 126 | 'id' => 123456789_123456789_123456789 127 | } 128 | post_json(req) 129 | 130 | last_response.should be_ok 131 | resp = MultiJson.decode(last_response.body) 132 | resp.should == { 133 | 'jsonrpc' => '2.0', 134 | 'result' => 4, 135 | 'id' => 123456789_123456789_123456789 136 | } 137 | end 138 | end 139 | end 140 | 141 | describe "receiving a request with named parameters" do 142 | context "when no errors occur" do 143 | it "returns a response with 'result'" do 144 | req = { 145 | 'jsonrpc' => '2.0', 146 | 'method' => 'subtract', 147 | 'params' => {'subtrahend'=> 20, 'minuend' => 24}, 148 | 'id' => 1 149 | } 150 | post_json(req) 151 | 152 | last_response.should be_ok 153 | resp = MultiJson.decode(last_response.body) 154 | resp.should == { 155 | 'jsonrpc' => '2.0', 156 | 'result' => 4, 157 | 'id' => 1 158 | } 159 | end 160 | end 161 | end 162 | 163 | describe "receiving a notification" do 164 | context "when no errors occur" do 165 | it "returns no response" do 166 | req = { 167 | 'jsonrpc' => '2.0', 168 | 'method' => 'update', 169 | 'params' => [1,2,3,4,5] 170 | } 171 | post_json(req) 172 | last_response.body.should be_empty 173 | end 174 | end 175 | end 176 | 177 | describe "receiving a call for a non-existent method" do 178 | it "returns an error response" do 179 | req = { 180 | 'jsonrpc' => '2.0', 181 | 'method' => 'foobar', 182 | 'id' => 1 183 | } 184 | post_json(req) 185 | 186 | resp = MultiJson.decode(last_response.body) 187 | resp.should == { 188 | 'jsonrpc' => '2.0', 189 | 'error' => { 190 | 'code' => -32601, 191 | 'message' => "Method 'foobar' not found." 192 | }, 193 | 'id' => 1 194 | } 195 | end 196 | end 197 | 198 | describe "receiving a call for a method which exists but is not exposed" do 199 | it "returns an error response" do 200 | req = { 201 | 'jsonrpc' => '2.0', 202 | 'method' => 'object_id', 203 | 'id' => 1 204 | } 205 | post_json(req) 206 | 207 | resp = MultiJson.decode(last_response.body) 208 | resp.should == { 209 | 'jsonrpc' => '2.0', 210 | 'error' => { 211 | 'code' => -32601, 212 | 'message' => "Method 'object_id' not found." 213 | }, 214 | 'id' => 1 215 | } 216 | end 217 | end 218 | 219 | describe "receiving a call with the wrong number of params" do 220 | it "returns an error response" do 221 | req = { 222 | 'jsonrpc' => '2.0', 223 | 'method' => 'subtract', 224 | 'params' => [1,2,3], 225 | 'id' => 1 226 | } 227 | post_json(req) 228 | 229 | resp = MultiJson.decode(last_response.body) 230 | resp.should == { 231 | 'jsonrpc' => '2.0', 232 | 'error' => { 233 | 'code' => -32602, 234 | 'message' => 'Invalid method parameter(s).' 235 | }, 236 | 'id' => 1 237 | } 238 | end 239 | end 240 | 241 | describe "receiving a call for ugly method" do 242 | context "by default" do 243 | it "returns only global error without stack trace" do 244 | req = { 245 | 'jsonrpc' => '2.0', 246 | 'method' => 'ugly_method', 247 | 'id' => 1 248 | } 249 | post_json(req) 250 | 251 | resp = MultiJson.decode(last_response.body) 252 | resp.should == { 253 | 'jsonrpc' => '2.0', 254 | 'error' => { 255 | 'code' => -32099, 256 | 'message' => 'Server application error' 257 | }, 258 | 'id' => 1 259 | } 260 | end 261 | end 262 | 263 | context "with 'show_errors' enabled" do 264 | it "returns an error name and first line of the stack trace" do 265 | req = { 266 | 'jsonrpc' => '2.0', 267 | 'method' => 'ugly_method', 268 | 'id' => 1 269 | } 270 | 271 | app = Server.new(router, :environment => "production", :show_errors => true) 272 | 273 | # have to make a new Rack::Test browser since this server is different than the normal one 274 | browser = Rack::Test::Session.new(Rack::MockSession.new(app)) 275 | browser.post '/', MultiJson.encode(req), {'Content-Type' => 'application/json'} 276 | 277 | resp = MultiJson.decode(browser.last_response.body) 278 | resp.should == { 279 | 'jsonrpc' => '2.0', 280 | 'error' => { 281 | 'code' => -32099, 282 | 'message' => "Server application error: RuntimeError at #{__FILE__}:40:in `ugly_method'" 283 | }, 284 | 'id' => 1 285 | } 286 | end 287 | end 288 | end 289 | 290 | describe "receiving invalid JSON" do 291 | it "returns an error response" do 292 | req = MultiJson.encode({ 293 | 'jsonrpc' => '2.0', 294 | 'method' => 'foobar', 295 | 'id' => 1 296 | }) 297 | req += '}' # make the json invalid 298 | post '/', req, {'Content-type' => 'application/json'} 299 | 300 | resp = MultiJson.decode(last_response.body) 301 | resp.should == { 302 | 'jsonrpc' => '2.0', 303 | 'error' => { 304 | 'code' => -32700, 305 | 'message' => 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.' 306 | }, 307 | 'id' => nil 308 | } 309 | end 310 | end 311 | 312 | describe "receiving an invalid request" do 313 | context "when the request is not a batch" do 314 | it "returns an error response" do 315 | req = { 316 | 'jsonrpc' => '2.0', 317 | 'method' => 1 # method as int is invalid 318 | } 319 | post_json(req) 320 | resp = MultiJson.decode(last_response.body) 321 | resp.should == INVALID_RESPONSE_EXPECTATION 322 | end 323 | end 324 | 325 | context "when the request is an empty batch" do 326 | it "returns an error response" do 327 | req = [] 328 | post_json(req) 329 | resp = MultiJson.decode(last_response.body) 330 | resp.should == INVALID_RESPONSE_EXPECTATION 331 | end 332 | end 333 | 334 | context "when the request is an invalid batch" do 335 | it "returns an error response" do 336 | req = [1,2] 337 | post_json(req) 338 | resp = MultiJson.decode(last_response.body) 339 | resp.should == [INVALID_RESPONSE_EXPECTATION, INVALID_RESPONSE_EXPECTATION] 340 | end 341 | end 342 | end 343 | 344 | describe "receiving a valid batch request" do 345 | context "when not all requests are notifications" do 346 | it "returns an array of responses" do 347 | reqs = [ 348 | {'jsonrpc' => '2.0', 'method' => 'sum', 'params' => [1,2,4], 'id' => '1'}, 349 | {'jsonrpc' => '2.0', 'method' => 'notify_hello', 'params' => [7]}, 350 | {'jsonrpc' => '2.0', 'method' => 'subtract', 'params' => [42,23], 'id' => '2'}, 351 | {'foo' => 'boo'}, 352 | {'jsonrpc' => '2.0', 'method' => 'foo.get', 'params' => {'name' => 'myself'}, 'id' => '5'}, 353 | {'jsonrpc' => '2.0', 'method' => 'get_data', 'id' => '9'} 354 | ] 355 | post_json(reqs) 356 | resp = MultiJson.decode(last_response.body) 357 | resp.should == [ 358 | {'jsonrpc' => '2.0', 'result' => 7, 'id' => '1'}, 359 | {'jsonrpc' => '2.0', 'result' => 19, 'id' => '2'}, 360 | {'jsonrpc' => '2.0', 'error' => {'code' => -32600, 'message' => 'The JSON sent is not a valid Request object.'}, 'id' => nil}, 361 | {'jsonrpc' => '2.0', 'error' => {'code' => -32601, 'message' => "Method 'foo.get' not found."}, 'id' => '5'}, 362 | {'jsonrpc' => '2.0', 'result' => ['hello', 5], 'id' => '9'} 363 | ] 364 | end 365 | end 366 | 367 | context "when all the requests are notifications" do 368 | it "returns no response" do 369 | req = [ 370 | { 371 | 'jsonrpc' => '2.0', 372 | 'method' => 'update', 373 | 'params' => [1,2,3,4,5] 374 | }, 375 | { 376 | 'jsonrpc' => '2.0', 377 | 'method' => 'update', 378 | 'params' => [1,2,3,4,5] 379 | } 380 | ] 381 | post_json(req) 382 | last_response.body.should be_empty 383 | end 384 | end 385 | end 386 | 387 | describe "receiving a 'system.' request" do 388 | context "when the request is 'isAlive'" do 389 | it "returns response 'true'" do 390 | req = { 391 | 'jsonrpc' => '2.0', 392 | 'method' => 'system.isAlive', 393 | 'params' => [], 394 | 'id' => 1 395 | } 396 | post_json(req) 397 | 398 | last_response.should be_ok 399 | resp = MultiJson.decode(last_response.body) 400 | resp.should == { 401 | 'jsonrpc' => '2.0', 402 | 'result' => true, 403 | 'id' => 1 404 | } 405 | end 406 | end 407 | context "when the request is 'system.listMethods'" do 408 | it "returns response with all jimson_exposed_methods on the handler(s) as strings" do 409 | req = { 410 | 'jsonrpc' => '2.0', 411 | 'method' => 'system.listMethods', 412 | 'params' => [], 413 | 'id' => 1 414 | } 415 | post_json(req) 416 | 417 | last_response.should be_ok 418 | resp = MultiJson.decode(last_response.body) 419 | resp['jsonrpc'].should == '2.0' 420 | resp['id'].should == 1 421 | expected = ['get_data', 'notify_hello', 'subtract', 'sum', 'car', 'ugly_method', 'update', 'system.isAlive', 'system.listMethods', 'other.multiply'] 422 | (resp['result'] - expected).should == [] 423 | end 424 | end 425 | end 426 | 427 | describe ".with_routes" do 428 | it "creates a server with a router by passing the block to Router#draw" do 429 | app = Server.with_routes do 430 | root TestHandler.new 431 | namespace 'foo', OtherHandler.new 432 | end 433 | 434 | # have to make a new Rack::Test browser since this server is different than the normal one 435 | browser = Rack::Test::Session.new(Rack::MockSession.new(app)) 436 | 437 | req = { 438 | 'jsonrpc' => '2.0', 439 | 'method' => 'foo.multiply', 440 | 'params' => [2, 3], 441 | 'id' => 1 442 | } 443 | browser.post '/', MultiJson.encode(req), {'Content-Type' => 'application/json'} 444 | 445 | browser.last_response.should be_ok 446 | resp = MultiJson.decode(browser.last_response.body) 447 | resp.should == { 448 | 'jsonrpc' => '2.0', 449 | 'result' => 6, 450 | 'id' => 1 451 | } 452 | end 453 | 454 | context "when opts are given" do 455 | it "passes the opts to the new server" do 456 | app = Server.with_routes(:show_errors => true) do 457 | root TestHandler.new 458 | namespace 'foo', OtherHandler.new 459 | end 460 | 461 | app.show_errors.should be_true 462 | end 463 | end 464 | end 465 | end 466 | end 467 | --------------------------------------------------------------------------------