jsonrpc2 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/README.markdown +176 -0
- data/Rakefile +2 -0
- data/example/config.ru +34 -0
- data/jsonrpc2.gemspec +55 -0
- data/lib/jsonrpc2.rb +4 -0
- data/lib/jsonrpc2/accept.rb +67 -0
- data/lib/jsonrpc2/auth.rb +66 -0
- data/lib/jsonrpc2/client.rb +59 -0
- data/lib/jsonrpc2/html.rb +137 -0
- data/lib/jsonrpc2/interface.rb +298 -0
- data/lib/jsonrpc2/textile.rb +131 -0
- data/lib/jsonrpc2/types.rb +174 -0
- data/lib/jsonrpc2/version.rb +5 -0
- metadata +156 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# JSON-RPC2 Ruby Server
|
2
|
+
|
3
|
+
A Rack compatible, documenting JSON-RPC 2 DSL/server implementation for ruby.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
* Inline documentation
|
8
|
+
* Type checking for parameters and return values
|
9
|
+
* Authentication support - including HTTP Basic Authentication
|
10
|
+
* Rack mountable
|
11
|
+
* Interactive browser based API testing
|
12
|
+
|
13
|
+
## Example
|
14
|
+
|
15
|
+
class Calculator < JSONRPC2::Interface
|
16
|
+
title "JSON-RPC2 Calculator"
|
17
|
+
introduction "This interface allows basic maths calculations via JSON-RPC2"
|
18
|
+
auth_with JSONRPC2::BasicAuth.new({'apiuser' => 'secretword'})
|
19
|
+
|
20
|
+
section 'Simple Ops' do
|
21
|
+
desc 'Multiply two numbers'
|
22
|
+
param 'a', 'Number', 'First number'
|
23
|
+
param 'b', 'Number', 'Second number'
|
24
|
+
result 'Number', 'a * b'
|
25
|
+
def mul args
|
26
|
+
args['a'] * args['b']
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'Add numbers'
|
30
|
+
param 'a', 'Number', 'First number'
|
31
|
+
param 'b', 'Number', 'Second number'
|
32
|
+
optional 'c', 'Number', 'Third number'
|
33
|
+
example 'Calculate 1 + 1', :params => { 'a' => 1, 'b' => 1}, :result => 2
|
34
|
+
result 'Number', 'a + b + c'
|
35
|
+
def sum args
|
36
|
+
val = args['a'] + args['b']
|
37
|
+
val += args['c'] if args['c']
|
38
|
+
val
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
To run example:
|
44
|
+
|
45
|
+
$ gem install shotgun # unless it's already installed
|
46
|
+
$ shotgun example/config.ru
|
47
|
+
|
48
|
+
Browse API and test it via a web browser at http://localhost:9393/
|
49
|
+
|
50
|
+
|
51
|
+
## Inline documentation
|
52
|
+
|
53
|
+
Use built in helper methods to declare complex types and function
|
54
|
+
parameters before defining method calls.
|
55
|
+
|
56
|
+
### Custom type definitions
|
57
|
+
|
58
|
+
e.g.
|
59
|
+
|
60
|
+
type "Address" do |t|
|
61
|
+
t.string "street", "Street name"
|
62
|
+
t.string "city", "City"
|
63
|
+
...
|
64
|
+
end
|
65
|
+
|
66
|
+
type "Person" do |t|
|
67
|
+
t.string "name", "Person's name"
|
68
|
+
t.number "age", "Person's age"
|
69
|
+
t.boolean "is_member", "Is person a member of our club?"
|
70
|
+
t.optional do
|
71
|
+
t.field "address", "Address", "Address of person"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
#### type "Name", &block
|
76
|
+
|
77
|
+
> Declare a JSON object type with named keys and value types
|
78
|
+
|
79
|
+
#### field "Name", "Type", "Description"
|
80
|
+
#### string "Name", "Description"
|
81
|
+
#### number "Name", "Description"
|
82
|
+
#### integer "Name", "Description"
|
83
|
+
#### boolean "Name", "Description"
|
84
|
+
|
85
|
+
> Describes the members of a JSON object - fields can be of any known type (see {JSONRPC2::Types} for details).
|
86
|
+
|
87
|
+
#### optional &block
|
88
|
+
#### required &block
|
89
|
+
|
90
|
+
> Use blocks to specify whether an object field is required or optional
|
91
|
+
|
92
|
+
---
|
93
|
+
|
94
|
+
### Method annotations
|
95
|
+
|
96
|
+
e.g.
|
97
|
+
|
98
|
+
desc 'Add numbers'
|
99
|
+
param 'a', 'Number', 'First number'
|
100
|
+
param 'b', 'Number', 'Second number'
|
101
|
+
optional 'c', 'Number', 'Third number'
|
102
|
+
example 'Calculate 1 + 1', :params => { 'a' => 1, 'b' => 1}, :result => 2
|
103
|
+
result 'Number', 'a + b + c'
|
104
|
+
def sum args
|
105
|
+
val = args['a'] + args['b']
|
106
|
+
val += args['c'] if args['c']
|
107
|
+
val
|
108
|
+
end
|
109
|
+
|
110
|
+
#### desc "Description of method"
|
111
|
+
|
112
|
+
> Short description of what the method does
|
113
|
+
|
114
|
+
#### param "Name", "Type", "Description"
|
115
|
+
|
116
|
+
or
|
117
|
+
|
118
|
+
#### optional "Name", "Type", "Description"
|
119
|
+
|
120
|
+
> Description of a named parameter for the method, including type and purpose (see {JSONRPC2::Types} for type details)
|
121
|
+
|
122
|
+
#### result "Type", "Description"
|
123
|
+
|
124
|
+
> Type and description of return value for method (see {JSONRPC2::Types} for type details)
|
125
|
+
|
126
|
+
#### example "Description", "Detail"
|
127
|
+
|
128
|
+
> Describe example usage
|
129
|
+
|
130
|
+
#### example "Description", { :params => { ... }, :result => value }
|
131
|
+
|
132
|
+
> Describe example usage and valid result (NB: values specified for both params and result are checked against the method type descriptions and generating docs will throw an error if the values are invalid).
|
133
|
+
|
134
|
+
#### example "Description", { :params => { ... }, :error => value }
|
135
|
+
|
136
|
+
> Describe example usage and sample error (NB: values specified for params are checked against the method type descriptions and generating docs throws an error if the values are invalid).
|
137
|
+
|
138
|
+
#### nodoc
|
139
|
+
|
140
|
+
> Don't include next method in documentation
|
141
|
+
|
142
|
+
---
|
143
|
+
|
144
|
+
### Interface annotations
|
145
|
+
|
146
|
+
e.g.
|
147
|
+
|
148
|
+
title "Calculator interface"
|
149
|
+
introduction "Very simple calculator interface"
|
150
|
+
|
151
|
+
section "Entry points" do
|
152
|
+
...
|
153
|
+
end
|
154
|
+
|
155
|
+
#### title "Title"
|
156
|
+
|
157
|
+
> Set title for interface
|
158
|
+
|
159
|
+
#### introduction "Introduction/description of interface"
|
160
|
+
|
161
|
+
> Add a basic introduction for the API
|
162
|
+
|
163
|
+
#### section "Name", &block
|
164
|
+
|
165
|
+
> Group methods into logical sections for documentation purposes
|
166
|
+
|
167
|
+
### Authentication
|
168
|
+
|
169
|
+
#### auth_with Authenticator
|
170
|
+
|
171
|
+
e.g.
|
172
|
+
auth_with JSONRPC2::BasicAuth.new({'apiuser' => 'secretword'})
|
173
|
+
|
174
|
+
> Specify authentication method that should be used to verify the access credentials before printing. See {JSONRPC2::BasicAuth} for examples/info.
|
175
|
+
|
176
|
+
|
data/Rakefile
ADDED
data/example/config.ru
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
$: << File.join(File.dirname(__FILE__),'../lib')
|
2
|
+
require 'jsonrpc2/interface'
|
3
|
+
|
4
|
+
class ::Object::Calculator < JSONRPC2::Interface
|
5
|
+
title "JSON-RPC2 Calculator"
|
6
|
+
introduction "This interface allows basic maths calculations via JSON-RPC2"
|
7
|
+
auth_with JSONRPC2::BasicAuth.new({'user' => 'secretword'})
|
8
|
+
|
9
|
+
section 'Simple Ops' do
|
10
|
+
desc 'Multiply two numbers'
|
11
|
+
param 'a', 'Number', 'a'
|
12
|
+
param 'b', 'Number', 'b'
|
13
|
+
result 'Number', 'a * b'
|
14
|
+
def mul args
|
15
|
+
args['a'] * args['b']
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Add numbers'
|
19
|
+
example "Calculate 1 + 1 = 2", :params => { 'a' => 1, 'b' => 1}, :result => 2
|
20
|
+
|
21
|
+
param 'a', 'Number', 'First number'
|
22
|
+
param 'b', 'Number', 'Second number'
|
23
|
+
optional 'c', 'Number', 'Third number'
|
24
|
+
result 'Number', 'a + b + c'
|
25
|
+
def sum args
|
26
|
+
val = args['a'] + args['b']
|
27
|
+
val += args['c'] if args['c']
|
28
|
+
val
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
run Calculator
|
34
|
+
|
data/jsonrpc2.gemspec
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/jsonrpc2/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Geoff Youngs"]
|
6
|
+
gem.email = ["git@intersect-uk.co.uk"]
|
7
|
+
gem.description = <<-EOD
|
8
|
+
JSON-RPC2 server DSL - allows APIs to be created as mountable Rack applications
|
9
|
+
with inline documentation, authentication and type checking.
|
10
|
+
|
11
|
+
e.g.
|
12
|
+
|
13
|
+
class Calculator < JSONRPC2::Interface
|
14
|
+
title "JSON-RPC2 Calculator"
|
15
|
+
introduction "This interface allows basic maths calculations via JSON-RPC2"
|
16
|
+
auth_with JSONRPC2::BasicAuth.new({'user' => 'secretword'})
|
17
|
+
|
18
|
+
section 'Simple Ops' do
|
19
|
+
desc 'Multiply two numbers'
|
20
|
+
param 'a', 'Number', 'a'
|
21
|
+
param 'b', 'Number', 'b'
|
22
|
+
result 'Number', 'a * b'
|
23
|
+
def mul args
|
24
|
+
args['a'] * args['b']
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'Add numbers'
|
28
|
+
example "Calculate 1 + 1 = 2", :params => { 'a' => 1, 'b' => 1}, :result => 2
|
29
|
+
|
30
|
+
param 'a', 'Number', 'First number'
|
31
|
+
param 'b', 'Number', 'Second number'
|
32
|
+
optional 'c', 'Number', 'Third number'
|
33
|
+
result 'Number', 'a + b + c'
|
34
|
+
def sum args
|
35
|
+
val = args['a'] + args['b']
|
36
|
+
val += args['c'] if args['c']
|
37
|
+
val
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
EOD
|
43
|
+
gem.summary = %q{JSON-RPC2 server DSL}
|
44
|
+
gem.homepage = "http://github.com/geoffyoungs/jsonrpc2"
|
45
|
+
|
46
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
47
|
+
gem.files = `git ls-files`.split("\n")
|
48
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
49
|
+
gem.name = "jsonrpc2"
|
50
|
+
gem.require_paths = ["lib"]
|
51
|
+
gem.version = JSONRPC2::VERSION
|
52
|
+
gem.add_dependency("httpclient")
|
53
|
+
gem.add_dependency("json")
|
54
|
+
gem.add_development_dependency("RedCloth")
|
55
|
+
end
|
data/lib/jsonrpc2.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Utils for parsing HTTP fields
|
2
|
+
module JSONRPC2::HTTPUtils
|
3
|
+
module_function
|
4
|
+
# Converts */* -> /^.*?/.*?$/
|
5
|
+
# text/* -> /^text\/.*?$/
|
6
|
+
# text/html -> /^text\/html$/
|
7
|
+
#
|
8
|
+
# @param [String] type Media type descriptor
|
9
|
+
# @return [Regexp] Regular expression that matches type
|
10
|
+
def type_to_regex type
|
11
|
+
case type
|
12
|
+
when /[*]/
|
13
|
+
Regexp.new("^#{type.split(/[*]/).map { |bit| Regexp.quote(bit) }.join('.*?')}$")
|
14
|
+
else
|
15
|
+
Regexp.new("^#{Regexp.quote(type)}$")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parses the HTTP Accept field and returns a sorted list of prefered
|
20
|
+
# types
|
21
|
+
#
|
22
|
+
# @param field
|
23
|
+
def parse_accept field, regex = false
|
24
|
+
index = -1
|
25
|
+
list = field.split(/,\s*/).map do |media|
|
26
|
+
index += 1
|
27
|
+
case media
|
28
|
+
when /;/
|
29
|
+
media, param_str = *media.split(/\s*;\s*(?=q\s*=)/,2)
|
30
|
+
params = param_str.to_s.split(/\s*;\s*/).inject({}) { |hash, str|
|
31
|
+
k,v = *str.strip.split(/=/).map(&:strip)
|
32
|
+
hash.merge(k => v)
|
33
|
+
}
|
34
|
+
{ :q => (params['q'] || 1.0).to_f, :media => media, :index => index }
|
35
|
+
else
|
36
|
+
{ :q => 1.0, :media => media, :index => index }
|
37
|
+
end
|
38
|
+
end.sort_by { |option| [-1 * option[:q], option[:media].scan(/[*]/).size, option[:index]] }
|
39
|
+
|
40
|
+
final = {}
|
41
|
+
list.each do |item|
|
42
|
+
q = item[:q]
|
43
|
+
final[q] ||= []
|
44
|
+
final[q].push(regex ? type_to_regex(item[:media]) : item[:media])
|
45
|
+
end
|
46
|
+
|
47
|
+
final.sort_by { |k,v| -1 * k }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Selects the clients preferred media/mime type based on Accept header
|
51
|
+
#
|
52
|
+
# @param [String] http_client_accepts HTTP Accepts header
|
53
|
+
# @param [Array<String>] options Media types available
|
54
|
+
def which http_client_accepts, options
|
55
|
+
return nil unless http_client_accepts
|
56
|
+
|
57
|
+
parse_accept(http_client_accepts, true).each do |preference, types|
|
58
|
+
types.each do |type|
|
59
|
+
options.each do |option|
|
60
|
+
return option if type.match(option)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module JSONRPC2
|
2
|
+
|
3
|
+
# @abstract Base authentication class
|
4
|
+
class Auth
|
5
|
+
# Validate an API request
|
6
|
+
#
|
7
|
+
#
|
8
|
+
def check(env, rpc)
|
9
|
+
true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# @abstract Base class for http-based authentication methods, e.g.
|
14
|
+
# {BasicAuth}
|
15
|
+
class HttpAuth < Auth
|
16
|
+
end
|
17
|
+
|
18
|
+
# HTTP Basic authentication implementation
|
19
|
+
class BasicAuth < HttpAuth
|
20
|
+
# Create a BasicAuth object
|
21
|
+
#
|
22
|
+
# @param [Hash<String,String>] users containing containing usernames and passwords
|
23
|
+
# @yield [user, pass] Username and password to authenticate
|
24
|
+
# @yieldreturn [Boolean] True if credentials are approved
|
25
|
+
def initialize(users=nil, &block)
|
26
|
+
@users, @lookup = users, block
|
27
|
+
end
|
28
|
+
|
29
|
+
# Checks that the client is authorised to access the API
|
30
|
+
#
|
31
|
+
# @param [Hash,Rack::Request] env Rack environment hash
|
32
|
+
# @param [Hash] rpc JSON-RPC2 call content
|
33
|
+
# @return [true] Returns true or throws :rack_response, [ 401, ... ]
|
34
|
+
def check(env, rpc)
|
35
|
+
valid?(env) or
|
36
|
+
throw(:rack_response, [401, {
|
37
|
+
'Content-Type' => 'text/html',
|
38
|
+
'WWW-Authenticate' => 'Basic realm="API"'
|
39
|
+
}, ["<html><head/><body>Authentication Required</body></html>"]])
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid?(env)
|
43
|
+
auth = env['HTTP_AUTHORIZATION']
|
44
|
+
|
45
|
+
return false unless auth
|
46
|
+
|
47
|
+
m = /Basic\s+([A-Za-z0-9+\/]+=*)/.match(auth)
|
48
|
+
user, pass = Base64.decode64(m[1]).split(/:/, 2)
|
49
|
+
user_valid?(user, pass)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Checks users hash and then the block given to the constructor to
|
53
|
+
# verify username / password.
|
54
|
+
def user_valid?(user, pass)
|
55
|
+
if @users && @users.respond_to?(:[])
|
56
|
+
if expected = @users[user]
|
57
|
+
return pass == expected
|
58
|
+
end
|
59
|
+
end
|
60
|
+
if @lookup
|
61
|
+
return @lookup.call(user, pass)
|
62
|
+
end
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'httpclient'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module JSONRPC2
|
5
|
+
# JSON RPC client error
|
6
|
+
class RemoteError < RuntimeError
|
7
|
+
end
|
8
|
+
|
9
|
+
# Simple JSONRPC client
|
10
|
+
class Client
|
11
|
+
# Create client object
|
12
|
+
#
|
13
|
+
# @param [String] uri Create client object
|
14
|
+
# @param [Hash] options Global options
|
15
|
+
def initialize(uri, options = {})
|
16
|
+
@uri = uri
|
17
|
+
@client = HTTPClient.new
|
18
|
+
@options = options
|
19
|
+
@id = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
# Call method with named arguments
|
23
|
+
# @param [String] method Remote method name
|
24
|
+
# @param [Hash<String,Value>] args Hash of named arguments for function
|
25
|
+
# @param [Hash<String,String>] options Additional parameters
|
26
|
+
# @return Method call result
|
27
|
+
# @raise [RemoteError] Error thrown by API
|
28
|
+
# @raise Transport/Network/HTTP errors
|
29
|
+
def call(method, args = {}, options = {}, &block)
|
30
|
+
headers = { 'Content-Type' => 'application/json-rpc' }
|
31
|
+
|
32
|
+
# Merge one level of hashes - ie. merge :headers
|
33
|
+
options = @options.merge(options) { |key,v1,v2| v2 = v1.merge(v2) if v1.class == v2.class && v1.is_a?(Hash); v2 }
|
34
|
+
|
35
|
+
if options[:headers]
|
36
|
+
headers = headers.merge(options[:headers])
|
37
|
+
end
|
38
|
+
|
39
|
+
if options[:auth]
|
40
|
+
@client.set_auth(@uri, options[:auth][:username], options[:auth][:password])
|
41
|
+
end
|
42
|
+
result = @client.post(@uri,
|
43
|
+
{ 'method' => method, 'params' => args, 'jsonrpc' => '2.0', 'id' => (@id+=1) }.to_json,
|
44
|
+
headers)
|
45
|
+
if result.contenttype =~ /^application\/json/
|
46
|
+
body = result.body
|
47
|
+
body = body.content if body.respond_to?(:content) #
|
48
|
+
data = JSON.parse body
|
49
|
+
if data.has_key?('result')
|
50
|
+
return data['result']
|
51
|
+
else
|
52
|
+
raise RemoteError, data['error']['message']
|
53
|
+
end
|
54
|
+
else
|
55
|
+
raise result.body
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|