saorin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ vendor
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in saorin.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 mashiro
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # Saorin
2
+
3
+ JSON-RPC server and client library.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'saorin'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install saorin
18
+
19
+ ## Usage
20
+
21
+ ### Server
22
+ ```ruby
23
+ class Handler
24
+ def hello(name)
25
+ "Helo #{name}!"
26
+ end
27
+ end
28
+
29
+ Saorin::Server.start Handler.new, :host => '0.0.0.0', :port => 8080
30
+ ```
31
+
32
+ ### Client
33
+ ```ruby
34
+ client = Saorin::Client.new :url => 'http://localhost:8080'
35
+ client.call :hello, 'trape' #=> 'Hello trape!'
36
+ ```
37
+
38
+ ## Contributing
39
+
40
+ 1. Fork it
41
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
42
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
43
+ 4. Push to the branch (`git push origin my-new-feature`)
44
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ task :default => :spec
data/lib/saorin.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'saorin/version'
2
+ require 'saorin/error'
3
+ require 'saorin/request'
4
+ require 'saorin/response'
5
+ require 'saorin/adapters'
6
+ require 'saorin/server'
7
+ require 'saorin/client'
8
+
9
+ module Saorin
10
+ JSON_RPC_VERSION = '2.0'
11
+ end
@@ -0,0 +1,2 @@
1
+ require 'saorin/adapters/servers'
2
+ require 'saorin/adapters/clients'
@@ -0,0 +1,10 @@
1
+ require 'saorin/adapters/registerable'
2
+
3
+ module Saorin
4
+ module Adapters
5
+ module Clients
6
+ include Registerable
7
+ self.load_path = 'saorin/adapters/clients'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,88 @@
1
+ require 'multi_json'
2
+ require 'saorin/request'
3
+ require 'saorin/response'
4
+ require 'saorin/adapters/clients'
5
+
6
+ module Saorin
7
+ module Adapters
8
+ module Clients
9
+ class Base
10
+ def initialize(options = {})
11
+ end
12
+
13
+ def call_single(method, *args)
14
+ request = Saorin::Request.new method, args, seqid!
15
+ response = send_request request.to_json
16
+ content = process_response response
17
+ raise content if content.is_a?(Saorin::RPCError)
18
+ content
19
+ end
20
+
21
+ alias_method :call, :call_single
22
+
23
+ def call_multi(*args)
24
+ requests = args.map do |arg|
25
+ request_args = arg.to_a.flatten
26
+ method = request_args.shift
27
+ Saorin::Request.new method, request_args, seqid!
28
+ end
29
+ response = send_request requests.to_json
30
+ process_response response
31
+ end
32
+
33
+ def send_request(content)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def process_response(content)
38
+ response = parse_response content
39
+ if response.is_a?(::Array)
40
+ response.map { |res| to_content handle_response(res) }
41
+ else
42
+ to_content handle_response(response)
43
+ end
44
+ rescue => e
45
+ raise Saorin::InvalidResponse, e.to_s
46
+ end
47
+
48
+ def parse_response(content)
49
+ MultiJson.decode content
50
+ rescue MultiJson::LoadError => e
51
+ raise Saorin::InvalidResponse, e.to_s
52
+ end
53
+
54
+ def to_content(res)
55
+ if res.error?
56
+ code = res.error['code']
57
+ error_class = Saorin.code_to_error code
58
+ error_class.new
59
+ else
60
+ res.result
61
+ end
62
+ end
63
+
64
+ def handle_response(hash)
65
+ response = Response.from_hash(hash)
66
+ response.validate
67
+ response
68
+ end
69
+
70
+ def seqid
71
+ @@seqid ||= 0
72
+ end
73
+
74
+ def seqid=(value)
75
+ @@seqid = value
76
+ @@seqid = 0 if @@seqid >= (1 << 31)
77
+ @@seqid
78
+ end
79
+
80
+ def seqid!
81
+ id = self.seqid
82
+ self.seqid += 1
83
+ id
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,28 @@
1
+ require 'saorin/adapters/clients/base'
2
+ require 'faraday'
3
+
4
+ module Saorin
5
+ module Adapters
6
+ module Clients
7
+ class Faraday < Base
8
+ attr_reader :connection
9
+
10
+ def initialize(options = {}, &block)
11
+ super options
12
+
13
+ @connection = ::Faraday::Connection.new(options) do |builder|
14
+ builder.adapter ::Faraday.default_adapter
15
+ block.call builder if block
16
+ end
17
+ end
18
+
19
+ def send_request(content)
20
+ response = @connection.post '', content
21
+ response.body
22
+ end
23
+ end
24
+
25
+ register :faraday, Faraday
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ require 'saorin/error'
2
+
3
+ module Saorin
4
+ module Adapters
5
+ module Registerable
6
+ class << self
7
+ def included(base)
8
+ base.extend ClassMethods
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ attr_accessor :load_path
14
+
15
+ def adapters
16
+ @adapters ||= {}
17
+ end
18
+
19
+ def register(key, adapter)
20
+ adapters[key.to_s] = adapter
21
+ end
22
+
23
+ def guess(key)
24
+ key = key.to_s
25
+ require "#{load_path}/#{key}"
26
+ adapter = adapters[key]
27
+ raise AdapterNotFound, key unless adapter
28
+ adapter
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ require 'saorin/adapters/registerable'
2
+
3
+ module Saorin
4
+ module Adapters
5
+ module Servers
6
+ include Registerable
7
+ self.load_path = 'saorin/adapters/servers'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,87 @@
1
+ require 'saorin/error'
2
+ require 'saorin/request'
3
+ require 'saorin/response'
4
+ require 'saorin/adapters/servers'
5
+
6
+ module Saorin
7
+ module Adapters
8
+ module Servers
9
+ class Base
10
+ attr_reader :handler, :allowed_methods
11
+
12
+ def initialize(handler, options = {})
13
+ @handler = handler
14
+ @allowed_methods = options[:allowed_methods] || handler.public_methods(false)
15
+ @allowed_methods.map! { |m| m.to_s }
16
+ end
17
+
18
+ def process_request(content)
19
+ response = begin
20
+ request = parse_request content
21
+ if request.is_a?(::Array)
22
+ raise Saorin::InvalidRequest if request.empty?
23
+ responses = request.map { |req| handle_request(req) }
24
+ responses.compact!
25
+ responses.empty? ? nil : responses
26
+ else
27
+ handle_request(request)
28
+ end
29
+ rescue Saorin::Error => e
30
+ Response.new(nil, e)
31
+ end
32
+
33
+ response && MultiJson.dump(response)
34
+ end
35
+
36
+ def parse_request(content)
37
+ MultiJson.decode content
38
+ rescue MultiJson::LoadError
39
+ raise Saorin::ParseError
40
+ end
41
+
42
+ def handle_request(hash)
43
+ begin
44
+ request = Request.from_hash(hash)
45
+ request.validate
46
+ result = dispatch_request request
47
+ response = Response.new(result, nil, request.id)
48
+ notify?(hash) ? nil : response
49
+ rescue Saorin::InvalidRequest => e
50
+ Response.new(nil, e)
51
+ rescue Saorin::Error => e
52
+ Response.new(nil, e, request.id)
53
+ rescue Exception => e
54
+ p e
55
+ Response.new(nil, Saorin::InternalError.new, request && request.id)
56
+ end
57
+ end
58
+
59
+ def notify?(hash)
60
+ hash.is_a?(::Hash) && !hash.has_key?('id')
61
+ end
62
+
63
+ def dispatch_request(request)
64
+ method = request.method.to_s
65
+ params = request.params || []
66
+
67
+ unless @allowed_methods.include?(method) &&
68
+ @handler.respond_to?(method)
69
+ raise Saorin::MethodNotFound
70
+ end
71
+
72
+ begin
73
+ if params.is_a?(::Hash)
74
+ @handler.__send__ method, params
75
+ else
76
+ @handler.__send__ method, *params
77
+ end
78
+ rescue ArgumentError
79
+ raise Saorin::InvalidParams
80
+ rescue Exception => e
81
+ raise Saorin::ServerError, e
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,31 @@
1
+ require 'saorin/adapters/servers/base'
2
+ require 'rack'
3
+
4
+ module Saorin
5
+ module Adapters
6
+ module Servers
7
+ class Rack < Base
8
+ DEFAULT_HEADERS = {
9
+ 'Content-Type' => 'application/json'
10
+ }
11
+
12
+ def initialize(handler, options = {}, &block)
13
+ super handler, options
14
+
15
+ ::Rack::Server.start({
16
+ :app => self
17
+ }.merge(options))
18
+ end
19
+
20
+ def call(env)
21
+ request = ::Rack::Request.new(env)
22
+ response = ::Rack::Response.new([], 200, DEFAULT_HEADERS)
23
+ response.write process_request(request.body.read) if request.post?
24
+ response.finish
25
+ end
26
+ end
27
+
28
+ register :rack, Rack
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ require 'saorin/adapters/servers/base'
2
+ require 'reel'
3
+
4
+ module Saorin
5
+ module Adapters
6
+ module Servers
7
+ class Reel < Base
8
+ DEFAULT_HEADERS = {
9
+ 'Content-Type' => 'application/json'
10
+ }
11
+
12
+ def initialize(handler, options = {}, &block)
13
+ super handler, options
14
+
15
+ server = ::Reel::Server.supervise(options[:host], options[:port], &method(:process))
16
+ trap(:INT) { server.terminate; exit }
17
+ sleep
18
+ end
19
+
20
+ def process(connection)
21
+ while request = connection.request
22
+ case request
23
+ when ::Reel::Request
24
+ response_body = ''
25
+ response_body = process_request(request.body) if request.method.to_s.upcase == 'POST'
26
+ request.respond ::Reel::Response.new(200, DEFAULT_HEADERS, response_body)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ register :reel, Reel
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ require 'saorin/adapters/clients'
2
+
3
+ module Saorin
4
+ class Client
5
+ class << self
6
+ def new(options = {}, &block)
7
+ adapter = options.delete(:adapter) || :faraday
8
+ adapter_class = Saorin::Adapters::Clients.guess adapter
9
+ adapter_class.new options, &block
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,69 @@
1
+ require 'multi_json'
2
+
3
+ module Saorin
4
+ class Error < StandardError
5
+ end
6
+
7
+ class RPCError < Error
8
+ attr_reader :code
9
+
10
+ def initialize(code, message)
11
+ @code = code
12
+ super message
13
+ end
14
+
15
+ def to_h
16
+ {'code' => code, 'message' => message}
17
+ end
18
+
19
+ def to_json(*args)
20
+ options = args.last.is_a?(::Hash) ? args.pop : {}
21
+ MultiJson.dump to_h, options
22
+ end
23
+ end
24
+
25
+ # code message meaning
26
+ # -32700 Parse error Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
27
+ # -32600 Invalid Request The JSON sent is not a valid Request object.
28
+ # -32601 Method not found The method does not exist / is not available.
29
+ # -32602 Invalid params Invalid method parameter(s).
30
+ # -32603 Internal error Internal JSON-RPC error.
31
+ # -32000 to -32099 Server error Reserved for implementation-defined server-errors.
32
+
33
+ JSON_RPC_ERRORS = [
34
+ [-32700, :ParseError, 'Parse error'],
35
+ [-32600, :InvalidRequest, 'Invalid Request'],
36
+ [-32601, :MethodNotFound, 'Method not found'],
37
+ [-32602, :InvalidParams, 'Invalid params'],
38
+ [-32603, :InternalError, 'Internal error'],
39
+ ]
40
+
41
+ JSON_RPC_ERRORS.each do |code, name, message|
42
+ class_eval <<-EOS
43
+ class #{name} < RPCError
44
+ def initialize
45
+ super #{code}, '#{message}'
46
+ end
47
+ end
48
+ EOS
49
+ end
50
+
51
+ class ServerError < RPCError
52
+ def initialize(e, code = -32000)
53
+ super code, e.to_s
54
+ end
55
+ end
56
+
57
+ class << self
58
+ def code_to_error(code)
59
+ @map ||= Hash[JSON_RPC_ERRORS.map { |c, n, m| [c, const_get(n)] }]
60
+ @map[code]
61
+ end
62
+ end
63
+
64
+ class InvalidResponse < Error
65
+ end
66
+
67
+ class AdapterNotFound < Error
68
+ end
69
+ end
@@ -0,0 +1,50 @@
1
+ require 'saorin'
2
+ require 'saorin/error'
3
+ require 'multi_json'
4
+
5
+ module Saorin
6
+ class Request
7
+ attr_accessor :version, :method, :params, :id
8
+
9
+ def initialize(method, params, id = nil, version = Saorin::JSON_RPC_VERSION)
10
+ @version = version
11
+ @method = method
12
+ @params = params
13
+ @id = id
14
+ end
15
+
16
+ def valid?
17
+ return false unless @method && @version
18
+ return false unless [String].any? { |type| @version.is_a? type }
19
+ return false unless [String].any? { |type| @method.is_a? type }
20
+ return false unless [Hash, Array, NilClass].any? { |type| @params.is_a? type }
21
+ return false unless [String, Numeric, NilClass].any? { |type| @id.is_a? type }
22
+ return false unless @version == JSON_RPC_VERSION
23
+ return false unless !@method.start_with?('.')
24
+ true
25
+ end
26
+
27
+ def validate
28
+ raise Saorin::InvalidRequest unless valid?
29
+ end
30
+
31
+ def to_h
32
+ h = {}
33
+ h['jsonrpc'] = @version
34
+ h['method'] = @method
35
+ h['params'] = @params if @params && !@params.empty?
36
+ h['id'] = @id
37
+ h
38
+ end
39
+
40
+ def to_json(*args)
41
+ options = args.last.is_a?(::Hash) ? args.pop : {}
42
+ MultiJson.dump to_h, options
43
+ end
44
+
45
+ def self.from_hash(hash)
46
+ raise Saorin::InvalidRequest unless hash.is_a?(::Hash)
47
+ new *hash.values_at('method', 'params', 'id', 'jsonrpc')
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,53 @@
1
+ require 'saorin'
2
+ require 'saorin/error'
3
+ require 'multi_json'
4
+
5
+ module Saorin
6
+ class Response
7
+ attr_accessor :version, :result, :error, :id
8
+
9
+ def initialize(result, error, id = nil, version = Saorin::JSON_RPC_VERSION)
10
+ @version = version
11
+ @result = result
12
+ @error = error
13
+ @id = id
14
+ end
15
+
16
+ def error?
17
+ !!@error
18
+ end
19
+
20
+ def valid?
21
+ return false unless (@result || @error) && !(@result && @error)
22
+ return false unless [String].any? { |type| @version.is_a? type }
23
+ return false unless [String, NilClass].any? { |type| @result.is_a? type }
24
+ return false unless [Saorin::Error, Hash, NilClass].any? { |type| @error.is_a? type }
25
+ return false unless [String, Numeric, NilClass].any? { |type| @id.is_a? type }
26
+ return false unless @version == JSON_RPC_VERSION
27
+ true
28
+ end
29
+
30
+ def validate
31
+ raise Saorin::InvalidResponse unless valid?
32
+ end
33
+
34
+ def to_h
35
+ h = {}
36
+ h['jsonrpc'] = @version
37
+ h['result'] = @result unless error?
38
+ h['error'] = @error if error?
39
+ h['id'] = id
40
+ h
41
+ end
42
+
43
+ def to_json(*args)
44
+ options = args.last.is_a?(::Hash) ? args.pop : {}
45
+ MultiJson.dump to_h, options
46
+ end
47
+
48
+ def self.from_hash(hash)
49
+ raise Saorin::InvalidResponse unless hash.is_a?(::Hash)
50
+ new *hash.values_at('result', 'error', 'id', 'jsonrpc')
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,15 @@
1
+ require 'saorin/adapters/servers'
2
+
3
+ module Saorin
4
+ class Server
5
+ class << self
6
+ def new(handler, options = {}, &block)
7
+ adapter = options.delete(:adapter) || :rack
8
+ adapter_class = Saorin::Adapters::Servers.guess adapter
9
+ adapter_class.new handler, options, &block
10
+ end
11
+
12
+ alias_method :start, :new
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Saorin
2
+ VERSION = '0.1.0'
3
+ end
data/saorin.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'saorin/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'saorin'
8
+ gem.version = Saorin::VERSION
9
+ gem.authors = ['mashiro']
10
+ gem.email = ['mail@mashiro.org']
11
+ gem.description = %q{JSON-RPC 2.0 implementation for ruby}
12
+ gem.summary = %q{JSON-RPC 2.0 implementation for ruby}
13
+ gem.homepage = ''
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_dependency 'multi_json'
21
+ gem.add_dependency 'faraday'
22
+ gem.add_development_dependency 'rake'
23
+ gem.add_development_dependency 'rspec'
24
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+ require 'saorin/adapters/servers/base'
3
+
4
+ describe Saorin::Adapters::Servers::Base do
5
+ it_should_behave_like 'rpc call' do
6
+ let(:process) do
7
+ handler = Handler.new
8
+ server = Saorin::Adapters::Servers::Base.new handler
9
+ proc do |input|
10
+ server.process_request input
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe Saorin::Request do
4
+ def create_request(options = {})
5
+ default_options = {
6
+ :version => Saorin::JSON_RPC_VERSION,
7
+ :method => 'test',
8
+ :params => [1, 2, 3],
9
+ :id => rand(1 << 31),
10
+ }
11
+ options = default_options.merge(options)
12
+ Saorin::Request.new *options.values_at(:method, :params, :id, :version)
13
+ end
14
+
15
+ describe '#initialize' do
16
+ before { @r = create_request :method => 'test', :params => [1, 2, 3], :id => 123 }
17
+ subject { @r }
18
+ its(:version) { should eq Saorin::JSON_RPC_VERSION }
19
+ its(:method) { should eq 'test' }
20
+ its(:params) { should eq [1, 2, 3] }
21
+ its(:id) { should eq 123 }
22
+ end
23
+
24
+ describe '#valid' do
25
+ it 'version' do
26
+ create_request(:version => '1.0').should_not be_valid
27
+ create_request(:version => '2.0').should be_valid
28
+ create_request(:version => 2).should_not be_valid
29
+ end
30
+
31
+ it 'method' do
32
+ create_request(:method => '.test').should_not be_valid
33
+ create_request(:method => 'test').should be_valid
34
+ create_request(:method => 123).should_not be_valid
35
+ end
36
+
37
+ it 'params' do
38
+ create_request(:params => 123).should_not be_valid
39
+ create_request(:params => '123').should_not be_valid
40
+ create_request(:params => nil).should be_valid
41
+ create_request(:params => []).should be_valid
42
+ create_request(:params => {}).should be_valid
43
+ end
44
+
45
+ it 'id' do
46
+ create_request(:id => 123).should be_valid
47
+ create_request(:id => '123').should be_valid
48
+ create_request(:id => nil).should be_valid
49
+ create_request(:id => []).should_not be_valid
50
+ create_request(:id => {}).should_not be_valid
51
+ end
52
+ end
53
+
54
+ describe '#validate' do
55
+ context 'valid' do
56
+ it do
57
+ r = create_request
58
+ r.stub(:valid?).and_return(true)
59
+ lambda { r.validate }.should_not raise_error Saorin::InvalidRequest
60
+ end
61
+ end
62
+
63
+ context 'invalid' do
64
+ it do
65
+ r = create_request
66
+ r.stub(:valid?).and_return(false)
67
+ lambda { r.validate }.should raise_error Saorin::InvalidRequest
68
+ end
69
+ end
70
+ end
71
+
72
+ describe '#to_h' do
73
+ it 'with params' do
74
+ h = create_request(:params => [1, 2, 3]).to_h
75
+ h.should include('params')
76
+ end
77
+ it 'without params' do
78
+ h1 = create_request(:params => nil).to_h
79
+ h2 = create_request(:params => []).to_h
80
+ h1.should_not include('params')
81
+ h2.should_not include('params')
82
+ end
83
+ end
84
+
85
+ describe '::from_hash' do
86
+ it 'convertible' do
87
+ r1 = create_request
88
+ h1 = r1.to_h
89
+ r2 = Saorin::Request.from_hash(h1)
90
+ h2 = r2.to_h
91
+ h1.should eq h2
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+ describe Saorin::Response do
4
+ def create_response(options = {})
5
+ default_options = {
6
+ :version => Saorin::JSON_RPC_VERSION,
7
+ :result => '123',
8
+ :error => nil,
9
+ :id => rand(1 << 31),
10
+ }
11
+ options = default_options.merge(options)
12
+ Saorin::Response.new *options.values_at(:result, :error, :id, :version)
13
+ end
14
+
15
+ describe '#initialize' do
16
+ context 'success' do
17
+ before { @r = create_response :result => '123', :error => nil, :id => 123 }
18
+ subject { @r }
19
+ its(:version) { should eq Saorin::JSON_RPC_VERSION }
20
+ its(:result) { should eq '123' }
21
+ its(:error) { should be_nil }
22
+ its(:id) { should eq 123 }
23
+ its(:error?) { should be_false }
24
+ end
25
+
26
+ context 'fail' do
27
+ before do
28
+ @e = Saorin::InvalidRequest.new
29
+ @r = create_response :result => nil, :error => @e, :id => 123
30
+ end
31
+ subject { @r }
32
+ its(:version) { should eq Saorin::JSON_RPC_VERSION }
33
+ its(:result) { should be_nil }
34
+ its(:error) { should eq @e }
35
+ its(:id) { should eq 123 }
36
+ its(:error?) { should be_true }
37
+ end
38
+ end
39
+
40
+ describe '#valid' do
41
+ it 'version' do
42
+ create_response(:version => '1.0').should_not be_valid
43
+ create_response(:version => '2.0').should be_valid
44
+ create_response(:version => 2).should_not be_valid
45
+ end
46
+
47
+ it 'result / error' do
48
+ create_response(:error => nil, :result => 123).should_not be_valid
49
+ create_response(:error => nil, :result => '123').should be_valid
50
+ create_response(:error => nil, :result => []).should_not be_valid
51
+ create_response(:error => nil, :result => {}).should_not be_valid
52
+
53
+ create_response(:result => nil, :error => 123).should_not be_valid
54
+ create_response(:result => nil, :error => '123').should_not be_valid
55
+ create_response(:result => nil, :error => nil).should_not be_valid
56
+ create_response(:result => nil, :error => []).should_not be_valid
57
+ create_response(:result => nil, :error => {}).should be_valid
58
+ create_response(:result => nil, :error => Saorin::InvalidResponse.new).should be_valid
59
+
60
+ create_response(:error => nil, :result => nil).should_not be_valid
61
+ create_response(:error => 123, :result => 123).should_not be_valid
62
+ end
63
+
64
+ it 'id' do
65
+ create_response(:id => 123).should be_valid
66
+ create_response(:id => '123').should be_valid
67
+ create_response(:id => nil).should be_valid
68
+ create_response(:id => []).should_not be_valid
69
+ create_response(:id => {}).should_not be_valid
70
+ end
71
+ end
72
+
73
+ describe '#validate' do
74
+ context 'valid' do
75
+ it do
76
+ r = create_response
77
+ r.stub(:valid?).and_return(true)
78
+ lambda { r.validate }.should_not raise_error Saorin::InvalidResponse
79
+ end
80
+ end
81
+
82
+ context 'invalid' do
83
+ it do
84
+ r = create_response
85
+ r.stub(:valid?).and_return(false)
86
+ lambda { r.validate }.should raise_error Saorin::InvalidResponse
87
+ end
88
+ end
89
+ end
90
+
91
+ describe '#to_h' do
92
+ it 'success' do
93
+ e = Saorin::InvalidRequest.new
94
+ h = create_response(:error => e).to_h
95
+ h.should_not include('result')
96
+ h.should include('error')
97
+ end
98
+ it 'fail' do
99
+ h = create_response(:error => nil).to_h
100
+ h.should include('result')
101
+ h.should_not include('error')
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,18 @@
1
+ require 'saorin'
2
+
3
+ # Requires supporting files with custom matchers and macros, etc,
4
+ # in ./support/ and its subdirectories.
5
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
6
+
7
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
8
+ RSpec.configure do |config|
9
+ config.treat_symbols_as_metadata_keys_with_true_values = true
10
+ config.run_all_when_everything_filtered = true
11
+ config.filter_run :focus
12
+
13
+ # Run specs in random order to surface order dependencies. If you find an
14
+ # order dependency and want to debug it, you can fix the order by providing
15
+ # the seed, which is printed after each run.
16
+ # --seed 1234
17
+ config.order = 'random'
18
+ end
@@ -0,0 +1,209 @@
1
+ require 'json'
2
+
3
+ class Handler
4
+ def subtract1(a, b)
5
+ a - b
6
+ end
7
+
8
+ def subtract2(options)
9
+ options['minuend'] - options['subtrahend']
10
+ end
11
+
12
+ def update(a, b, c, d, e)
13
+ 'OK'
14
+ end
15
+
16
+ def foobar1
17
+ 'OK'
18
+ end
19
+
20
+ def foobar3(a, b)
21
+ 'OK'
22
+ end
23
+
24
+ def sum(a, b, c)
25
+ a + b + c
26
+ end
27
+
28
+ def notify_hello(a)
29
+ 'OK'
30
+ end
31
+
32
+ def get_data
33
+ ['hello', 5]
34
+ end
35
+
36
+ def notify_sum(a, b, c)
37
+ a + b + c
38
+ end
39
+ end
40
+
41
+ def deserialize(data)
42
+ data && JSON.load(data)
43
+ end
44
+
45
+ def validates(process, inputs, answers)
46
+ inputs.zip(answers).each do |input, answer|
47
+ output = deserialize process.call(input)
48
+ answer = deserialize answer
49
+ output.should eq answer
50
+ end
51
+ end
52
+
53
+ shared_examples 'rpc call with positional parameters' do
54
+ it 'rpc call with positional parameters' do
55
+ inputs << %({"jsonrpc": "2.0", "method": "subtract1", "params": [42, 23], "id": 1})
56
+ answers << %({"jsonrpc": "2.0", "result": 19, "id": 1})
57
+ inputs << %({"jsonrpc": "2.0", "method": "subtract1", "params": [23, 42], "id": 2})
58
+ answers << %({"jsonrpc": "2.0", "result": -19, "id": 2})
59
+ validates process, inputs, answers
60
+ end
61
+ end
62
+
63
+ shared_examples 'rpc call with named parameters' do
64
+ it 'rpc call with named parameters' do
65
+ inputs << %({"jsonrpc": "2.0", "method": "subtract2", "params": {"subtrahend": 23, "minuend": 42}, "id": 3})
66
+ answers << %({"jsonrpc": "2.0", "result": 19, "id": 3})
67
+ inputs << %({"jsonrpc": "2.0", "method": "subtract2", "params": {"minuend": 42, "subtrahend": 23}, "id": 4})
68
+ answers << %({"jsonrpc": "2.0", "result": 19, "id": 4})
69
+ validates process, inputs, answers
70
+ end
71
+ end
72
+
73
+ shared_examples 'a Notification' do
74
+ it 'a Notification' do
75
+ inputs << %({"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]})
76
+ answers << nil
77
+ inputs << %({"jsonrpc": "2.0", "method": "foobar1"})
78
+ answers << nil
79
+ validates process, inputs, answers
80
+ end
81
+ end
82
+
83
+ shared_examples 'rpc call of non-existent method' do
84
+ it 'rpc call of non-existent method' do
85
+ inputs << %({"jsonrpc": "2.0", "method": "foobar2", "id": "1"})
86
+ answers << %({"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"})
87
+ validates process, inputs, answers
88
+ end
89
+ end
90
+
91
+ shared_examples 'rpc call with invalid JSON' do
92
+ it 'rpc call with invalid JSON' do
93
+ inputs << %({"jsonrpc": "2.0", "method": "foobar3, "params": "bar", "baz])
94
+ answers << %({"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null})
95
+ validates process, inputs, answers
96
+ end
97
+ end
98
+
99
+ shared_examples 'rpc call with invalid Request object' do
100
+ it 'rpc call with invalid Request object' do
101
+ inputs << %({"jsonrpc": "2.0", "method": 1, "params": "bar"})
102
+ answers << %({"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null})
103
+ validates process, inputs, answers
104
+ end
105
+ end
106
+
107
+ shared_examples 'rpc call Batch, invalid JSON' do
108
+ it 'rpc call Batch, invalid JSON' do
109
+ inputs << <<-JSON
110
+ [
111
+ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
112
+ {"jsonrpc": "2.0", "method"
113
+ ]
114
+ JSON
115
+ answers << <<-JSON
116
+ {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}
117
+ JSON
118
+ validates process, inputs, answers
119
+ end
120
+ end
121
+
122
+ shared_examples 'rpc call with an empty Array' do
123
+ it 'rpc call with an empty Array' do
124
+ inputs << %([])
125
+ answers << %({"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null})
126
+ validates process, inputs, answers
127
+ end
128
+ end
129
+
130
+ shared_examples 'rpc call with an invalid Batch (but not empty)' do
131
+ it 'rpc call with an invalid Batch (but not empty)' do
132
+ inputs << %([1])
133
+ answers << <<-JSON
134
+ [
135
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
136
+ ]
137
+ JSON
138
+ validates process, inputs, answers
139
+ end
140
+ end
141
+
142
+ shared_examples 'rpc call with invalid Batch' do
143
+ it 'rpc call with invalid Batch' do
144
+ inputs << %([1,2,3])
145
+ answers << <<-JSON
146
+ [
147
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
148
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
149
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
150
+ ]
151
+ JSON
152
+ validates process, inputs, answers
153
+ end
154
+ end
155
+
156
+ shared_examples 'rpc call Batch' do
157
+ it 'rpc call Batch' do
158
+ inputs << <<-JSON
159
+ [
160
+ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
161
+ {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
162
+ {"jsonrpc": "2.0", "method": "subtract1", "params": [42,23], "id": "2"},
163
+ {"foo": "boo"},
164
+ {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
165
+ {"jsonrpc": "2.0", "method": "get_data", "id": "9"}
166
+ ]
167
+ JSON
168
+ answers << <<-JSON
169
+ [
170
+ {"jsonrpc": "2.0", "result": 7, "id": "1"},
171
+ {"jsonrpc": "2.0", "result": 19, "id": "2"},
172
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
173
+ {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
174
+ {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
175
+ ]
176
+ JSON
177
+ validates process, inputs, answers
178
+ end
179
+ end
180
+
181
+ shared_examples 'rpc call Batch (all notifications)' do
182
+ it 'rpc call Batch (all notifications)' do
183
+ inputs << <<-JSON
184
+ [
185
+ {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]},
186
+ {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}
187
+ ]
188
+ JSON
189
+ answers << nil
190
+ validates process, inputs, answers
191
+ end
192
+ end
193
+
194
+ shared_examples 'rpc call' do
195
+ let(:inputs) { [] }
196
+ let(:answers) { [] }
197
+ include_examples 'rpc call with positional parameters'
198
+ include_examples 'rpc call with named parameters'
199
+ include_examples 'a Notification'
200
+ include_examples 'rpc call of non-existent method'
201
+ include_examples 'rpc call with invalid JSON'
202
+ include_examples 'rpc call with invalid Request object'
203
+ include_examples 'rpc call Batch, invalid JSON'
204
+ include_examples 'rpc call with an empty Array'
205
+ include_examples 'rpc call with an invalid Batch (but not empty)'
206
+ include_examples 'rpc call with invalid Batch'
207
+ include_examples 'rpc call Batch'
208
+ include_examples 'rpc call Batch (all notifications)'
209
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: saorin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - mashiro
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: multi_json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: faraday
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: JSON-RPC 2.0 implementation for ruby
79
+ email:
80
+ - mail@mashiro.org
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - .gitignore
86
+ - .rspec
87
+ - Gemfile
88
+ - LICENSE.txt
89
+ - README.md
90
+ - Rakefile
91
+ - lib/saorin.rb
92
+ - lib/saorin/adapters.rb
93
+ - lib/saorin/adapters/clients.rb
94
+ - lib/saorin/adapters/clients/base.rb
95
+ - lib/saorin/adapters/clients/faraday.rb
96
+ - lib/saorin/adapters/registerable.rb
97
+ - lib/saorin/adapters/servers.rb
98
+ - lib/saorin/adapters/servers/base.rb
99
+ - lib/saorin/adapters/servers/rack.rb
100
+ - lib/saorin/adapters/servers/reel.rb
101
+ - lib/saorin/client.rb
102
+ - lib/saorin/error.rb
103
+ - lib/saorin/request.rb
104
+ - lib/saorin/response.rb
105
+ - lib/saorin/server.rb
106
+ - lib/saorin/version.rb
107
+ - saorin.gemspec
108
+ - spec/adapter/base_spec.rb
109
+ - spec/request_spec.rb
110
+ - spec/response_spec.rb
111
+ - spec/spec_helper.rb
112
+ - spec/support/shared_examples.rb
113
+ homepage: ''
114
+ licenses: []
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ segments:
126
+ - 0
127
+ hash: -3052577034699129611
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ segments:
135
+ - 0
136
+ hash: -3052577034699129611
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 1.8.24
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: JSON-RPC 2.0 implementation for ruby
143
+ test_files:
144
+ - spec/adapter/base_spec.rb
145
+ - spec/request_spec.rb
146
+ - spec/response_spec.rb
147
+ - spec/spec_helper.rb
148
+ - spec/support/shared_examples.rb