jsonrpc2 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jsonrpc2.gemspec
4
+ gemspec
@@ -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
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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
+
@@ -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
@@ -0,0 +1,4 @@
1
+ require "jsonrpc2/version"
2
+
3
+ module JSONRPC2
4
+ end
@@ -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