jsonrpc2 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|